Lifespan¶
Lifespan is the ASGI startup/shutdown channel.
Why lifespan exists¶
Use it for process-wide resources:
- database pools
- cache clients
- outbound HTTP clients
- telemetry exporters
Example app:
from __future__ import annotations
import time
STATE = {"started_at": None}
async def app(scope, receive, send):
"""Handle lifespan and HTTP scopes in one callable."""
if scope["type"] == "lifespan":
while True:
message = await receive()
if message["type"] == "lifespan.startup":
STATE["started_at"] = time.time()
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
await send({"type": "lifespan.shutdown.complete"})
return
if scope["type"] == "http":
body = f"started_at={STATE['started_at']}".encode()
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [
(b"content-type", b"text/plain"),
(b"content-length", str(len(body)).encode("ascii")),
],
}
)
await send({"type": "http.response.body", "body": body})
Lifespan modes¶
--lifespan auto: run when app supports lifespan--lifespan on: require lifespan behavior--lifespan off: disable lifespan channel
Worker model impact¶
Each worker process runs its own lifespan cycle. If you run 4 workers, startup/shutdown hooks run 4 times.
Startup/shutdown expectations¶
Startup:
- Palfrey sends startup event
- app initializes resources
- app confirms startup complete
Shutdown:
- Palfrey stops accepting new work
- drains in-flight work within limits
- sends shutdown event
- app releases resources
Plain-language explanation¶
Lifespan is the opening and closing checklist for the app process. When done correctly, deploys and restarts become predictable.