Deploying aiohttp servers

aiohttp · Expert · 12 min

run_app is fine for development, but production needs multiple processes, proxies and clean shutdowns.

web.run_app is a convenience wrapper. In production you choose among:

  • run_app behind a reverse proxy (nginx, a cloud load balancer), with one process per CPU managed by systemd, supervisord or a container orchestrator. Each process can listen on its own port or a shared Unix socket (path=).
  • Gunicorn with aiohttp's worker class: gunicorn myapp:create_app --worker-class aiohttp.GunicornWebWorker --workers 4 --bind 0.0.0.0:8080. Gunicorn manages processes; each worker runs its own event loop. (The worker is also importable from aiohttp.worker.)
  • AppRunner and sites for embedding the server inside a larger asyncio program, for example alongside a background consumer:
runner = web.AppRunner(app); await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", 8080); await site.start()
...
await runner.cleanup()

Behind a proxy, the client IP and scheme come from headers like X-Forwarded-For. Use aiohttp-remotes (or your own middleware) to trust them only from the proxy.

Graceful shutdown: on SIGTERM, aiohttp stops accepting connections, runs on_shutdown, waits up to shutdown_timeout (default 60 seconds, configurable in run_app and AppRunner) for in-flight requests, cancels the rest, and runs on_cleanup. Close WebSockets in on_shutdown or they hold the process up.

Performance: install aiohttp[speedups] (aiodns, Brotli) and consider uvloop as the event loop.

Example

import asyncio
from aiohttp import web, ClientSession

async def hello(request):
    return web.Response(text="ok")

def create_app():                       # gunicorn myapp:create_app --worker-class aiohttp.GunicornWebWorker
    app = web.Application()
    app.router.add_get("/health", hello)
    return app

async def main():
    runner = web.AppRunner(create_app(), shutdown_timeout=10)
    await runner.setup()
    site = web.TCPSite(runner, "127.0.0.1", 8089)
    await site.start()                  # server runs inside this asyncio program
    try:
        async with ClientSession() as s, s.get("http://127.0.0.1:8089/health") as r:
            print(r.status, await r.text())   # 200 ok
        # ... other tasks (queue consumers, schedulers) could run here ...
    finally:
        await runner.cleanup()          # graceful shutdown: on_shutdown, wait, on_cleanup
    print("stopped")                    # stopped

asyncio.run(main())

Remember

  • run_app behind a proxy, Gunicorn with GunicornWebWorker, or AppRunner + TCPSite
  • Graceful shutdown waits up to shutdown_timeout, then runs cleanup
  • Trust X-Forwarded-* headers only from your proxy

Watch out for

  • Trusting X-Forwarded-For from any client, letting users spoof their IP.

Check yourself

  1. Why use web.AppRunner and TCPSite instead of web.run_app?

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

Official docs: docs.aiohttp.org/en/stable/deployment.html