App state, startup and cleanup

aiohttp · Advanced · 11 min

Database pools and HTTP clients must be created once at startup and closed at shutdown, not per request.

An Application is a mapping you can store shared objects in, such as database pools, a ClientSession for outgoing calls, or caches. Since aiohttp 3.9 the recommended keys are typed web.AppKey objects:

db_key = web.AppKey("db", asyncpg.Pool)
app[db_key] = pool
pool = request.app[db_key]    # type checkers know this is a Pool

String keys still work but emit a warning and lose type information.

Resources are created and destroyed via lifecycle signals:

  • app.cleanup_ctx: async generators that run setup code before yield at startup and teardown after yield at cleanup. This is the preferred style because setup and teardown live together and teardown only runs if setup succeeded.
  • app.on_startup, app.on_shutdown, app.on_cleanup: lists of coroutines called at each phase. on_shutdown runs first when stopping, a good time to close WebSockets; then on_cleanup.

After the app starts it is frozen: adding routes, middlewares or signals then raises an error. Modifying state values (app[key]) stays allowed.

Large apps can be split into sub-applications with app.add_subapp("/admin/", admin_app), each with its own middlewares and state, and nested apps reach the parent's state through request.config_dict, which looks keys up through the chain.

Per-request data goes on the request (request["user"]), never on the app, because the app is shared by all concurrent requests.

Example

import asyncio
import aiohttp
from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer

http_key = web.AppKey("http", aiohttp.ClientSession)
events = []

async def http_client(app):
    app[http_key] = aiohttp.ClientSession()      # setup at startup
    events.append("started")
    yield
    await app[http_key].close()                  # teardown at cleanup
    events.append("closed")

async def status(request):
    return web.json_response({"open": not request.app[http_key].closed})
async def demo():
    app = web.Application()
    app.cleanup_ctx.append(http_client)
    app.router.add_get("/status", status)
    async with TestClient(TestServer(app)) as c:
        print(await (await c.get("/status")).json())  # {'open': True}
    print(events)                                     # ['started', 'closed']
asyncio.run(demo())

Remember

  • Store shared resources in the app with typed web.AppKey keys
  • cleanup_ctx async generators pair setup and teardown
  • The app freezes at startup; per-request data goes on the request

Watch out for

  • Creating a ClientSession or DB pool inside each handler instead of once at startup.

Check yourself

  1. What happens if you call app.router.add_get(...) after the application has started?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.aiohttp.org/en/stable/web_advanced.html#aiohttp-web-signals