Introduction
Memory leaks are a silent killer for Python services in production. Even though the CPython interpreter uses reference counting, certain patterns can keep objects alive forever, leading to increased RSS and eventual OOM crashes. This guide walks you through detecting, diagnosing, and fixing Python memory leaks in a live environment.
Why Python Still Leaks
-
Reference cycles that the garbage collector can't clean because they contain objects with
__del__. -
Unbounded caches (e.g.,
functools.lru_cache,werkzeugcache) that grow without eviction. - Global state held by third‑party libraries.
- Native extensions leaking memory on the C side.
Step‑by‑Step Troubleshooting
1️⃣ Enable Tracing with tracemalloc
import tracemalloc
tracemalloc.start()
# ... run your workload ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[ Top 10 memory allocations ]")
for stat in top_stats[:10]:
print(stat)
2️⃣ Use the gc Module to Find Uncollectable Objects
import gc, pprint
gc.set_debug(gc.DEBUG_LEAK)
def dump_unreachable():
unreachable = gc.collect()
pprint.pprint(gc.garbage)
# Call `dump_unreachable()` after a suspicious load test
3️⃣ Visualise Object Graphs with objgraph
import objgraph
objgraph.show_most_common_types(limit=20)
objgraph.show_backrefs(
[obj for obj in gc.get_objects() if isinstance(obj, MyLeakyClass)],
filename='leak.png')
4️⃣ Pinpoint Hot Functions with memory_profiler
from memory_profiler import profile
@profile
def process_batch(batch):
# heavy processing logic
pass
Run the script with python -m memory_profiler my_script.py to see line‑by‑line memory usage.
5️⃣ Fix the Leak
Common patterns and their fixes:
| Pattern | Typical Fix |
|---|---|
A mutable default argument (def foo(bar=[])) |
Use None and initialise inside. |
Open file/DB connection without close()
|
Use with context manager. |
Large list or dict that never shrinks |
Explicitly clear() or replace with a bounded queue. |
Reference cycle with __del__
|
Remove __del__ or break the cycle manually. |
Example: Leaky Cache
# leaky.py – problematic version
from functools import lru_cache
@lru_cache(maxsize=None) # ← no bound → unbounded growth
def compute(value):
return heavy_computation(value)
Fix
# fixed.py – bounded cache
from functools import lru_cache
@lru_cache(maxsize=1024) # reasonable bound
def compute(value):
return heavy_computation(value)
Production‑Level Monitoring
- Export process RSS via Prometheus (
node_exporterorprocess-exporter). - Set alerts when RSS grows > 75 % of the container limit.
- Periodically dump a
tracemallocsnapshot and store it in a log bucket for post‑mortem analysis.
Automated Patch Tool
We’ve packaged a ready‑to‑run script that scans your codebase for common leak patterns and suggests fixes. Download the pre‑configured script here.
Or grab the full repository: Get the complete patch tool.
For a deeper dive, Access the full repository fix and adapt it to your stack.
Conclusion
Fixing memory leaks is a blend of good coding habits, systematic profiling, and continuous monitoring. Apply the checklist above, automate the detection steps, and keep your Python services healthy under real‑world load.
Happy debugging!
Top comments (0)