Uvicorn's 'uvicorn.error' Logger Is Not About Errors: Stop the False Alarms
Your log monitor pages you at 2am. Severity: error. Source: your FastAPI service. You SSH in expecting a stack trace and find this:
INFO [uvicorn.error] ('127.0.0.1', 47014) - "WebSocket /api" [accepted]
A websocket connected. That is the whole incident. A client did exactly what it was supposed to do, uvicorn logged it at INFO, and your monitoring tool saw the string uvicorn.error in the line and decided it was an error. It was not. Nothing was wrong. You are awake for nothing.
This is one of those bugs that is not a bug in your code, not a bug in uvicorn's behaviour, but a naming decision that reads as a lie to any tool that pattern-matches on the word "error". Once you understand what uvicorn.error actually names, the fix is small and permanent. The trap is that the obvious reaction, "suppress uvicorn.error", throws away your real errors along with the noise.
uvicorn.error is uvicorn's general internal logger, not a severity. The name mirrors gunicorn's access-log versus error-log split, where "error log" means "the server's own messages", not "things that went wrong". Startup, websocket accepts and shutdowns all log to it at INFO. So:
- Never alert on the logger name substring "error". Alert on the
levelnamefield (ERROR/CRITICAL) instead. - If your monitor only reads raw text, switch uvicorn to structured JSON logging so the level is a distinct field.
- Optionally rename the logger in your own
dictConfigbefore your rules see it.
The line that triggers the false alarm
Here are three routine events, all emitted by the same logger, none of them an error:
INFO [uvicorn.error] Started server process [12345]
INFO [uvicorn.error] Application startup complete.
INFO [uvicorn.error] ('127.0.0.1', 47014) - "WebSocket /api" [accepted]
Every one of those is normal operation. The word error in the bracket is the logger's name, printed by uvicorn's default format string, sitting right next to the level INFO that correctly says this is informational. A human skims past it. A regex that greps for error across log lines does not, and neither does a security tool like OSSEC whose rules pattern-match on substrings. Every websocket connection, every startup, becomes a page.
Why the logger is called that
uvicorn splits its logging into two loggers, deliberately, following gunicorn's convention:
| Logger | What it carries | What people assume |
|---|---|---|
uvicorn.access | The HTTP access log: one line per request with method, path, status. | correct: it is the access log |
uvicorn.error | Everything else uvicorn itself has to say: startup, shutdown, websocket lifecycle, reload notices, and yes, actual errors too. | wrong: they read "error" as severity |
In the gunicorn world this pairing is old and well understood: the "access log" records client requests, and the "error log" is where the server writes its own operational messages, which by long Unix tradition is the non-access stream regardless of severity. Think of it as the server's diary, not its error report. uvicorn adopted the same two-logger split and the same names, so uvicorn.error inherited a label that describes a category (uvicorn's own messages) while looking exactly like a severity.
The maintainers agree the name is a historical mistake. It has stayed because renaming it is a breaking change: countless deployments already reference uvicorn.error by name in their logging config, filters and handlers, and silently renaming it would break all of them at once. So it remains, and the correct move is on your side of the line.
The fix: alert on level, not on name
Step 1: Point your monitoring rule at the level field
The single most important change is in your monitoring tool, not your app. Whatever alerts you (OSSEC, a Loki rule, a Datadog monitor, a grep-based cron), stop matching the substring error anywhere in the line and start matching the severity. For a tool reading the default text format, anchor on the level token at the start of the line rather than the logger name in brackets. For anything structured, match the level field directly. The rule you want is "level is ERROR or CRITICAL", full stop.
Step 2: Emit structured JSON so the level is unambiguous
Text matching is fragile. Give your monitor a real field to match by switching uvicorn to JSON logs through a dictConfig. Here the level is levelname, a distinct key your rule can test exactly:
# log_config.py
import logging, json
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
return json.dumps({
"level": record.levelname, # INFO / WARNING / ERROR <- match THIS
"logger": record.name, # uvicorn.error, uvicorn.access, ...
"message": record.getMessage(),
})
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {"json": {"()": JsonFormatter}},
"handlers": {
"default": {"class": "logging.StreamHandler", "formatter": "json"},
},
"loggers": {
"uvicorn": {"handlers": ["default"], "level": "INFO"},
"uvicorn.error": {"handlers": ["default"], "level": "INFO", "propagate": False},
"uvicorn.access": {"handlers": ["default"], "level": "INFO", "propagate": False},
},
}
Run uvicorn with it:
uvicorn app:app --log-config log_config.py
Now the websocket-accepted line arrives as {"level": "INFO", "logger": "uvicorn.error", "message": "... [accepted]"}. Your monitor keys on level == "ERROR" and the noise disappears while genuine errors, which arrive as level: ERROR under the same logger, still fire.
Step 3 (optional): rename the logger in your own config
If a downstream tool is genuinely hard-wired to the logger name and you cannot change its rule, you can relabel the logger on your side before anything downstream sees it. The cleanest approach is a filter that rewrites the record's name:
class RenameUvicornError(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
if record.name == "uvicorn.error":
record.name = "uvicorn.server" # a name that is not a lie
return True
Attach that filter to the handler in your dictConfig. Uvicorn keeps writing to uvicorn.error internally; your handler renames the record to uvicorn.server on the way out, so your log store and monitor only ever see the honest name. This is a workaround for a specific stubborn tool, not the primary fix. The primary fix is still Step 1.
Verification
Prove both halves: routine events no longer alert, and real errors still do.
1. Routine event is quiet. Open a websocket to your service and confirm the accept line is classified as INFO by your pipeline, not as an alert:
uvicorn app:app --log-config log_config.py 2>&1 | grep -i websocket
# expect: {"level": "INFO", "logger": "uvicorn.error", "message": "... [accepted]"}
Your monitor should record it and stay silent.
2. Real error still fires. Raise an unhandled exception in a route and confirm it arrives as level: ERROR and does trigger the alert. This is the check people skip, and it is the important one: a fix that silences the false alarm by muting uvicorn.error entirely would also mute this. If your genuine 500 still pages you, the fix is correct.
What people get wrong
Suppressing the uvicorn.error logger to kill the noise. This is the tempting one-liner: set uvicorn.error to WARNING or drop it. It works right up until uvicorn logs an actual unhandled exception, a bind failure, or a protocol error to that same logger and you never see it. You have not fixed the false-positive problem, you have converted it into a false-negative problem, which is strictly worse because it fails silently.
Grepping logs for the word "error" as a health signal. This is the root cause of the whole ordeal. Log severity is a structured field that exists precisely so you do not have to guess from prose. Any monitoring rule built on "does the line contain the string error" will false-positive on uvicorn.error, on log lines that quote an error message in an INFO context, and on request paths that contain the word. Match the level.
Filing a bug asking uvicorn to rename it. Reasonable instinct, already discussed upstream, and not going to happen soon because the rename breaks everyone currently configuring logging by that name. Spend the energy on your own config, where the fix is one rule and permanent.
When it is still broken
- Check for a second logger doing the same thing. Gunicorn, when it fronts uvicorn workers, has its own
gunicorn.errorlogger with the identical naming quirk. If you moved your rule to level-matching and still get noise, you may have two "error" loggers and only fixed one. - Confirm the JSON actually reached your store. If your log shipper re-parses the message and re-derives severity from text, it can undo your structured level. Inspect a raw record in the log store, not the pretty-printed view, to see which field your rules run against. The habit of reading the raw event rather than the summary is the same one I lean on when reading Linux logs during a 3am outage.
- Watch
propagate. Ifuvicorn.errorpropagates to the root logger and the root has a handler with a different formatter, you can get the line twice, once structured and once as plain text, and the plain-text copy re-triggers a text rule. Setpropagate: Falseas in the config above. - Test the access logger too.
uvicorn.accessis fine by name, but a request to a path literally containing/errorwill still match a substring rule. Another reason to abandon substring matching entirely.
The whole episode is a small lesson with a large blast radius: a logger name is metadata about origin, not severity, and the moment your alerting confuses the two it will wake you up for websocket handshakes forever. Match on the level, emit structured logs so the level is unambiguous, and let uvicorn.error keep its unfortunate name in peace.
Frequently asked questions
- Why does uvicorn log INFO messages under the name uvicorn.error?
- Because uvicorn.error is uvicorn's general-purpose internal logger, not a severity marker. The name mirrors gunicorn's split between an access log and an error log, where 'error log' historically means 'the server's own messages' as opposed to the request access log. So startup notices, websocket accepts and shutdowns all go to uvicorn.error at INFO level, which is confusing but intentional.
- Is a log line tagged uvicorn.error actually an error?
- Not necessarily. The logger name tells you the category (uvicorn's internal messages) not the severity. To know whether something is really an error you must read the level field, which is INFO, WARNING or ERROR. A line like INFO [uvicorn.error] WebSocket accepted is a normal INFO event that happens to be emitted by the misleadingly named logger.
- How do I stop my log monitor from alerting on every uvicorn.error line?
- Alert on the levelname field, not on the logger name substring. Configure your monitoring rule to match records where level is ERROR or CRITICAL, and ignore the logger name entirely. If your tool only sees raw text, switch uvicorn to structured JSON logging so the level is a distinct field the rule can match on precisely.
- Will uvicorn ever rename the uvicorn.error logger?
- Unlikely in the near term. Maintainers acknowledge the name is a historical mistake, but renaming it would break every deployment that already filters or configures logging on that exact logger name. Because a rename is a breaking change for existing setups, the practical position is to work around it in your own logging config and monitoring rules rather than wait for it to change.