Django ASGI Production Checklist

A checklist for running Django under ASGI in production with Uvicorn, Daphne, Granian, or Hypercorn: choose workers, avoid blocking calls in async views, handle sync middleware and the ORM, configure WebSocket proxies and timeouts, lifespan, and graceful shutdown.

Problem statement

You switched Django to ASGI for async views, streaming responses, server-sent events, or WebSockets. Under ASGI, a single blocking call in an async view can stall every request on that worker, sync middleware silently adds thread hops, the ORM requires care in async contexts, and proxies need settings for long-lived connections. Performance can end up worse than WSGI if these details are missed.

ASGI in productionASGI serverUvicorn, GranianEvent loopnever block itSync bridgessync_to_asyncProxy + shutdownupgrades, timeoutsASGI pays off only when async code stays non-blocking end to end.
  1. Run an ASGI server with multiple worker processes (for example gunicorn -k uvicorn_worker.UvicornWorker or uvicorn --workers N, or Granian).
  2. Keep async views free of blocking I/O: use async clients (httpx, async Redis) and the async ORM API or sync_to_async.
  3. Prefer async-capable middleware; each sync middleware in the chain forces thread switching.
  4. Configure Nginx or the load balancer for WebSocket upgrades, streaming (no buffering), and long timeouts where needed.
  5. Handle lifespan events and SIGTERM so in-flight requests and connections close gracefully.

Checklist

Server and workers

  • ASGI server chosen and pinned: Uvicorn (with the uvicorn-worker Gunicorn class), Daphne for Channels, Granian, or Hypercorn.
  • Several worker processes per container or host to use multiple cores; one event loop per process.
  • --limit-max-requests or equivalent recycling to bound memory growth.
  • DJANGO_SETTINGS_MODULE and asgi.py used consistently; get_asgi_application() called before importing models.

Async correctness

  • No blocking calls in async def views: no requests, time.sleep, sync Redis clients, or sync ORM calls.
  • ORM in async code via async methods (aget, acount, async for) or wrapped in sync_to_async(thread_sensitive=True).
  • Sync views still work under ASGI (Django runs them in a thread pool), but heavy sync code reduces the benefit.
  • SynchronousOnlyOperation errors fixed properly, not suppressed with DJANGO_ALLOW_ASYNC_UNSAFE.
mistake and fixMistakeFixrequests.get in async viewhttpx.AsyncClientsync ORM in async viewasync ORM methods or sync_to_asyncmany sync middlewareasync-capable middlewaresingle worker processone loop per core, several workersOne blocking call stalls every request on that event loopdjango-deployment.com
Profile event loop lag to find hidden blocking calls.

Middleware and settings

  • Middleware reviewed for async support (async_capable); third-party middleware upgraded where possible.
  • CONN_MAX_AGE set thoughtfully: async views using sync_to_async share thread-local connections; persistent connections behave as under WSGI in those threads.
  • SECURE_PROXY_SSL_HEADER and forwarded headers configured for the proxy (--proxy-headers, --forwarded-allow-ips for Uvicorn).

Proxy and connections

  • Nginx: proxy_http_version 1.1, Upgrade and Connection headers for WebSockets, proxy_buffering off for streaming and SSE.
  • Timeouts long enough for streaming and WebSockets on those paths only (proxy_read_timeout), defaults elsewhere.
  • Load balancer idle timeouts above WebSocket ping intervals; clients reconnect with backoff.
  • Connection limits (worker_connections, file descriptors) sized for long-lived connections.

Lifespan and shutdown

  • Lifespan protocol handled or disabled explicitly (--lifespan off if the app does not use it, avoiding startup warnings).
  • Graceful shutdown timeout set; WebSocket clients notified or able to reconnect on deploy.
  • Health checks hit a cheap async endpoint; readiness reflects database availability.
pick the interface by workloadASGI pays off✓WebSockets, SSE, streaming✓many slow external calls✕needs async disciplineWSGI is enough✓classic request/response✓mostly sync ORM work✕no long-lived connectionsMostly sync apps gain little from ASGI and add complexitydjango-deployment.com
You can run both: WSGI for the main app and ASGI for real-time paths.

Explanation

ASGI servers run Django inside an event loop. Async views yield control while awaiting I/O, so one process can serve many concurrent slow requests. The model breaks down when something blocks the loop: a synchronous network call or ORM query in an async view freezes all requests on that loop until it returns. Django bridges sync and async code with thread pools (sync_to_async), which keeps correctness but adds overhead. Production readiness under ASGI is mostly about keeping async paths truly async and configuring proxies for long-lived connections.

Edge cases / notes

  • Channels. WebSocket apps with Channels need a channel layer (Redis) and their own checklist.
  • Celery from async views. task.delay() is synchronous network I/O; call it via sync_to_async or use an async-capable client.
  • Testing. Use AsyncClient and pytest-asyncio or Django's async test support for async views.

Next steps

FAQ

Do sync views work under ASGI?

Yes. Django runs them in a thread pool, but heavy sync workloads gain little from ASGI.

How many ASGI workers should I run?

Typically one process per CPU core, each with its own event loop.

Can I use the Django ORM in async views?

Yes, with async methods like aget and acount, or by wrapping sync code in sync_to_async.

Why is my ASGI app slower than WSGI?

Usually blocking calls in async views or many sync middleware forcing thread switches.

Keep going