Python Runtime Advanced: Native Memory, ASGI Lifecycles, and Worker Failure
Debug memory outside Python objects, choose ASGI worker boundaries, and find silent hangs in background consumers.
On this page
1. Python heap is not process RSS
tracemalloc tracks many Python-managed allocations, but C extensions, native libraries, allocator arenas, and memory-mapped files can contribute to resident set size without appearing as Python object growth. Compare snapshots with process RSS, allocator statistics, and extension-specific metrics. A stable Python snapshot with rising RSS suggests investigating native allocations, fragmentation, caches, or mapped memory rather than declaring the issue solved.
C extensions must follow reference-count ownership rules and handle borrowed references correctly. A missing decref leaks; an invalid lifetime can become a crash. Cython can accelerate typed loops, but benchmark the boundary conversions and verify memory ownership when calling C APIs. Performance work should include a reproducible profile and an equivalent correctness test.
2. ASGI versus WSGI lifecycle
WSGI is a synchronous request/response callable model. ASGI supports async request handling, concurrency, and lifespan startup/shutdown events. Uvicorn runs an event loop per worker process; Gunicorn can supervise multiple workers, but each worker has separate process memory and often its own database/client pools. Increasing workers multiplies connection limits and caches.
Create shared clients/pools during the app lifespan and close them at shutdown. A single global SQLAlchemy session is mutable transaction state and must not be shared across concurrent tasks. Use a session per request/task and bounded pool configuration based on total replicas and workers.
3. Queue worker that hangs
An asyncio consumer can wait forever on a queue, socket, or lock without raising. Name tasks, add heartbeats and operation deadlines, and wrap suspect awaits in asyncio.wait_for() or a timeout scope. Enable loop debug mode in a safe environment and inspect asyncio.all_tasks() stacks when the worker is stuck. For shutdown, install signal handling in the main thread, request cancellation, await cleanup within a deadline, and preserve unprocessed messages for redelivery.
If a task group has a child exception, structured concurrency cancels siblings and propagates an exception group when the scope exits. Retain a reference to the task group and handle expected errors at that boundary; detached tasks can fail without an owner observing the exception.
Interview and troubleshooting drills
Interview: Why is one global SQLAlchemy session unsafe in async FastAPI? Explain mutable identity-map/transaction state, concurrent access, request-scoped sessions, and connection-pool capacity across workers.
Troubleshooting: A queue worker stops progressing with no exception. Capture task stacks and loop lag, identify blocking sync code or a wait with no deadline, bound it with a timeout, then determine whether the message should retry, return to the queue, or move to a dead-letter path.
Challenge: A C extension retains native memory while tracemalloc remains flat. Explain why the tools disagree, what process-level evidence to collect, and how to isolate the allocation site without assuming every RSS increase is a Python object leak.