The Non-Blocking Fallacy

async def does not increase execution speed; it enables non-blocking I/O. Using synchronous libraries (like requests) inside an async function blocks the entire event loop, preventing it from handling concurrent requests. Always replace blocking I/O with asynchronous alternatives (e.g., httpx.AsyncClient) to ensure the event loop remains free to process other tasks.

Resource and Connection Management

In async environments, a single worker handles thousands of concurrent requests. Opening a new database connection per request leads to connection storms and exhaustion.

  • Connection Pooling: Use a persistent pool (e.g., asyncpg.create_pool) initialized at startup and shared across requests.
  • Resource Leaks: Failing to close async clients or connections leads to memory leaks that often manifest only after days of uptime. Use a lifespan context manager to initialize and clean up resources globally.
  • Process Supervision: Never run a single Uvicorn worker in production. Use Gunicorn with UvicornWorker to manage multiple processes. Set --max-requests and --max-requests-jitter to periodically restart workers, which serves as a safety net against undetected memory leaks.

Handling CPU-Bound Tasks

Async Python is designed for I/O-bound work. CPU-intensive tasks (image processing, data transformation) block the event loop entirely. Move these tasks to a ThreadPoolExecutor to offload them from the main loop, or for heavy workloads, offload them to a dedicated task queue like Celery or RQ.

Observability and Debugging

Default logs are insufficient for debugging distributed async failures. Implement structured logging with a unique request_id passed through contextvars. This allows you to correlate logs across the entire lifecycle of a request, making it possible to trace 500 errors back to specific user actions or downstream failures.