If a Python web worker’s memory keeps climbing under steady traffic, first check whether it is a real leak or expected growth from caches, workload, or Python’s allocator. A worker’s resident memory (RSS) may stay high even after objects are freed, so RSS alone doesn’t identify the cause.
Find where memory is growing
Reproduce the pattern with a consistent workload: warm up the application, send the same requests several times, and compare memory after each round. Track both process RSS and Python allocations. tracemalloc can show which Python source lines account for growing allocations:
import tracemalloc
# Start tracing early so allocations made during the test are recorded.
tracemalloc.start(25)
# Take this snapshot after warm-up, before sending the repeated workload.
before = tracemalloc.take_snapshot()
# Run the same request workload here, then allow short-lived work to finish.
after = tracemalloc.take_snapshot()
# Show source lines with the largest net increase in traced allocations.
for stat in after.compare_to(before, "lineno")[:10]:
print(stat)
Take snapshots in the same worker and compare equivalent periods; otherwise normal differences in traffic can mislead you. tracemalloc tracks Python-managed allocations, not every native allocation. If RSS rises but traced Python memory does not, investigate native extensions or allocator behavior with a tool such as Memray.
Common causes in web apps
- Unbounded state: module-level lists, dictionaries, or caches that retain request data. Put a size or expiry limit on caches, or use a dedicated cache service.
- Objects held longer than intended: request objects, ORM sessions, response data, or closures kept in global state or long-lived callbacks. Keep request-scoped state local and close sessions and other resources reliably.
- Background work that accumulates: queued tasks, unfinished async tasks, or callbacks that retain large objects. Check that tasks complete, errors are handled, and references are released.
- Native resources: database connections, file handles, image buffers, or extension-library allocations that aren’t released. Use context managers where available and inspect the relevant library’s lifecycle.
After changing the suspected code, repeat the same workload and compare snapshots again. A useful fix should stop the steady growth in live allocations. If the application is stable but RSS does not fall immediately, that alone doesn’t mean the leak remains—Python may keep freed memory available for later allocations.