Fix "ASGI lifespan protocol appears unsupported" Warnings

Understand and silence the Uvicorn message ASGI lifespan protocol appears unsupported when serving Django: why Django does not handle lifespan events, how to turn lifespan off, and where to put startup code instead.

Problem statement

Every time Uvicorn (or Gunicorn with Uvicorn workers) starts your Django ASGI app, the log shows:

INFO:     Waiting for application startup.
INFO:     ASGI 'lifespan' protocol appears unsupported.
INFO:     Application startup complete.

Some teams see it as a warning in their log aggregator and assume startup failed. It did not. The ASGI lifespan protocol lets a server tell the application "starting up" and "shutting down" so it can open and close resources. Django's built-in ASGI handler does not implement those events, so Uvicorn tries, gets no answer, logs this line, and carries on serving requests normally.

What the lifespan probe doesUvicorn startssends lifespan.startupDjango handlerignores lifespanUvicorn notes itlogs unsupportedRequests servednormallyThe message is informational: Django simply does not use lifespan events.
  1. The message is harmless; your app is serving requests.
  2. Silence it with --lifespan off on Uvicorn, or lifespan="off" in config.
  3. With Gunicorn and Uvicorn workers, use a custom worker class that sets lifespan: off, or the uvicorn-worker package's equivalent.
  4. Put startup code in AppConfig.ready() (lightweight only) or in your entrypoint, not in lifespan handlers.
  5. Do not set --lifespan on: it turns the harmless message into a startup failure.

Step-by-step solution

1. Confirm requests work

curl -sI http://127.0.0.1:8000/health/ | head -1

If you get a response, startup is fine. The lifespan message appears between "Waiting for application startup" and "Application startup complete" and does not stop either.

2. Turn lifespan off in Uvicorn

uvicorn config.asgi:application --host 127.0.0.1 --port 8000 --lifespan off

or programmatically:

uvicorn.run("config.asgi:application", host="127.0.0.1", port=8000, lifespan="off")

The three values are auto (default: try, and log if unsupported), on (require it; fail if the app does not support it), and off (never send lifespan events).

3. Gunicorn with Uvicorn workers

uvicorn.workers.UvicornWorker (now also published as the separate uvicorn-worker package) accepts config through a subclass:

# config/workers.py
from uvicorn_worker import UvicornWorker

class DjangoUvicornWorker(UvicornWorker):
    CONFIG_KWARGS = {"lifespan": "off", "loop": "auto", "http": "auto"}
gunicorn config.asgi:application -k config.workers.DjangoUvicornWorker --bind unix:/run/gunicorn/gunicorn.sock
lifespan setting vs Django ASGI handlerWhat happensUse it?auto (default)probe fails, info logged!fine, but noisyoffno probe, no messageโœ“recommendedonstartup failsโœ•never with DjangoOff matches what Django supports; on makes startup depend on a feature it lacksdjango-deployment.com
Set lifespan off explicitly so log alerts do not trip on the message.

4. Put startup work in the right place

Code people try to put in lifespan handlers, and where it belongs in Django instead:

  • Signal handlers, checks, registries: AppConfig.ready(). Keep it fast and do not query the database there.
  • Warming caches, migrations, collectstatic: a deploy step or the container entrypoint before the server starts.
  • Opening shared clients (HTTP sessions, SDKs): lazily on first use, stored at module level.
  • Cleanup on shutdown: usually unnecessary; the process exit closes sockets. For Celery or other workers, handle SIGTERM in those processes.

5. If you really need lifespan events

Wrap Django's ASGI application in a small router that handles lifespan itself and passes everything else to Django:

# config/asgi.py
import os
from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
django_app = get_asgi_application()

async def application(scope, receive, send):
    if scope["type"] == "lifespan":
        while True:
            message = await receive()
            if message["type"] == "lifespan.startup":
                # open resources here
                await send({"type": "lifespan.startup.complete"})
            elif message["type"] == "lifespan.shutdown":
                # close resources here
                await send({"type": "lifespan.shutdown.complete"})
                return
    else:
        await django_app(scope, receive, send)

Channels projects already use ProtocolTypeRouter; add the lifespan branch there in the same way.

a home for each kind of startup workDeploy step or entrypointmigrate, collectstatic, cache warmAppConfig.ready()signals, checks; no DB queriesLazy module-level clientscreated on first useCustom lifespan wrapperonly if truly neededMost projects never need lifespan events at alldjango-deployment.com
Keep server startup fast; do heavy work in the deploy pipeline.

Verification check: restart the service and confirm the log goes from Waiting for application startup straight to Application startup complete (with lifespan off, neither line appears), and that requests still return 200.

Explanation

The ASGI specification defines three scope types: http, websocket, and lifespan. Frameworks such as Starlette and FastAPI implement lifespan to run startup and shutdown hooks. Django's ASGIHandler handles only http and raises for other scope types, which Uvicorn's auto mode interprets as "lifespan not supported" and logs at info level. Nothing is broken; the server simply learns that it should not expect lifespan responses from this application.

Edge cases / notes

  • Hypercorn and Daphne handle missing lifespan support quietly or with their own messages; behaviour varies by version.
  • Granian has its own lifespan options for ASGI apps; check its --no-ws/interface flags and docs for the equivalent.
  • Log alert rules matching the word "unsupported" are a common reason this message causes pages. Turn lifespan off rather than writing exceptions in alert rules.
  • Future Django versions may add lifespan support; check release notes when upgrading.

Next steps

FAQ

Is ASGI lifespan unsupported an error?

No. It is an informational message. Django does not implement lifespan events, and the server continues normally.

How do I remove the message?

Run Uvicorn with --lifespan off, or set lifespan: off in a custom Uvicorn worker for Gunicorn.

Should I use lifespan on?

No. With Django, on makes startup fail because the handler does not answer lifespan events.

Where should Django startup code go?

Light setup in AppConfig.ready(); heavier tasks like migrations in a deploy step or entrypoint.

Keep going