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 fromaiohttp.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.