Skip to content

ASGI and Uvicorn

To use FastAPI well, you need to understand its runtime boundary before focusing on route syntax. FastAPI is not the server. It is an ASGI application. Uvicorn is the ASGI server that connects that application to real sockets, HTTP traffic, WebSocket connections, and process lifecycle.

Quick takeaway: Uvicorn accepts connections, runs the event loop, parses HTTP or WebSocket traffic, and calls the application through ASGI `scope`, `receive`, and `send`. FastAPI and Starlette sit on top of that and handle routing, dependency injection, validation, and serialization. You need this split to reason about workers, timeouts, proxies, and lifespan behavior.

The full request path

A FastAPI request does not start inside the route function. It flows through sockets, the ASGI server, middleware, dependencies, and response events.

Separate FastAPI from Uvicorn

LayerMain responsibilityQuestions that belong here
Uvicornsocket acceptance, event loop, protocol parsing, connection timeout, worker processeskeep-alive, graceful shutdown, proxy headers, concurrency limits
ASGI contractscope, receive, send, plus http/websocket/lifespanwhat event flow is actually happening
Starlette/FastAPImiddleware, routing, dependencies, validation, serialization, exception handlinghow thin routes should be, where resource ownership belongs

Start with the smallest ASGI shape

py
async def app(scope, receive, send):
    if scope["type"] != "http":
        return

    message = await receive()
    assert message["type"] == "http.request"

    await send(
        {
            "type": "http.response.start",
            "status": 200,
            "headers": [(b"content-type", b"text/plain; charset=utf-8")],
        }
    )
    await send(
        {
            "type": "http.response.body",
            "body": b"hello from ASGI",
        }
    )

The important point is that the app does not receive a framework request object directly. It receives a general protocol interface. FastAPI builds richer request and dependency abstractions on top of that interface.

What Uvicorn actually owns

1. It accepts connections and calls the ASGI app

  • It handles TCP connections and HTTP or WebSocket protocol parsing.
  • It turns parsed protocol data into ASGI scope values and inbound events.
  • It turns ASGI response messages back into bytes on the wire.

2. It coordinates lifespan at the server level

  • It opens lifespan on startup.
  • It closes lifespan on shutdown.
  • In multi-worker setups, each worker should be treated as running its own lifespan sequence.

3. It owns critical operational settings

SettingMeaningWhen it mattersCommon mistake
--reloadrestarts on code changeslocal developmentleaving it on in production
--workersprocess countCPU usage and isolationincreasing it without DB pool math
--proxy-headerstrust forwarded proxy headersproduction behind ingress or Nginxmisreading client IP or scheme
--timeout-keep-aliveidle keep-alive durationcontrolling connection churnchanging it blindly
--timeout-graceful-shutdownshutdown grace periodrolling deploys and drainingcutting requests during shutdown
--limit-concurrencyserver-side concurrency capdownstream protectionrelying only on app-level semaphores

Common points of confusion

Worker count and coroutine count are different axes

  • Workers are processes.
  • Coroutines are scheduled tasks inside a worker event loop.
  • Raising worker count does not automatically make route code proportionally faster.

app.state is not shared memory across workers

  • Each worker is a separate process.
  • Engines, clients, and in-memory caches created in lifespan exist per worker.
  • Assumptions like "this initializes only once" often break in multi-worker deployments.

Route code is not server behavior

  • Timeouts, keep-alive behavior, proxy handling, and graceful shutdown live in the ASGI server layer.
  • Validation, dependency graphs, and response modeling live in FastAPI and Starlette.

Basic environment profiles to distinguish

Local development

  • uvicorn app.main:app --reload
  • prioritize fast iteration
  • do not trust local performance numbers

Simple container deployment

  • disable reload
  • size workers together with DB connection budgets
  • configure forwarded headers explicitly when behind a proxy

WebSocket or streaming-heavy services

  • understand ASGI connection lifetime first
  • long-lived ASGI servers are often a more natural fit than request-only runtimes
  • revisit timeout, shutdown, and background-work assumptions with connection lifetime in mind
  1. Lifespan and Testing
  2. Background Tasks and Offloading
  3. Performance and Ops

For runnable intuition, pair this chapter with examples/asgi_lifecycle_lab.py.

Official References

Built with VitePress for a Python 3.14 handbook.