Introduction
Memory leaks in long‑running Python services can silently degrade performance, exhaust RAM, and cause costly outages. In this guide we walk through practical, production‑ready techniques to detect, isolate, and fix memory leaks without stopping the entire system.
Why Python Leaks Happen
- Holding references in global containers (lists, dicts, caches) after they are no longer needed.
- Cyclic references involving objects with
__del__methods. - Mis‑configured third‑party libraries that keep internal caches alive.
- Unclosed file/network handles that retain buffers.
Toolset Overview
| Tool | What It Does | When to Use |
|---|---|---|
tracemalloc |
Tracks memory allocations and provides snapshot diffs. | Quick, built‑in, low overhead. |
objgraph |
Visualizes object graphs and finds most common types. | Deep dive into reference cycles. |
guppy/heapy |
Heap analysis and size breakdown. | When you need precise size per type. |
memory_profiler |
Line‑by‑line memory usage (via @profile). |
Spotting hot spots in functions. |
Step‑by‑Step Troubleshooting
1️⃣ Reproduce the Leak in a Controlled Environment
Run a load test that mirrors production traffic and monitor RSS with psutil or top. Note the baseline memory and the point where it starts climbing.
# Example using psutil in a short script
python -c "import psutil, time, os; proc = psutil.Process(os.getpid());
for i in range(30):
time.sleep(10);
print('RSS:', proc.memory_info().rss/1024/1024, 'MiB')"
2️⃣ Enable tracemalloc Early
Add the following snippet to the entry point of your service (e.g., app.py). It adds negligible overhead and starts tracking from the very first import.
import tracemalloc
tracemalloc.start()
print('tracemalloc started, snapshot interval = 1 MB')
3️⃣ Capture Snapshots at Key Moments
Take a snapshot before the workload starts and another after the suspected leak period.
import tracemalloc, time
snapshot_start = tracemalloc.take_snapshot()
# … run your workload …
time.sleep(300) # simulate 5‑minute load
snapshot_end = tracemalloc.take_snapshot()
# Compare the two snapshots
top_stats = snapshot_end.compare_to(snapshot_start, 'lineno')
print('--- Top 10 memory‑growing lines ---')
for stat in top_stats[:10]:
print(stat)
The output will list the source files and line numbers that allocated the most additional memory.
4️⃣ Drill Down with objgraph
If the offending line allocates objects that later stay alive, visualize the reference chain:
import objgraph
# After reproducing the leak, run:
objgraph.show_backrefs(
objgraph.by_type('MyLeakyClass')[0],
max_depth=5,
filename='leak_backrefs.png')
print('Back‑reference graph saved to leak_backrefs.png')
Open the PNG to see which objects keep MyLeakyClass alive.
5️⃣ Apply the Fix
Common patterns and their remedies:
-
Cache without eviction → Use
functools.lru_cache(maxsize=…)or a time‑based eviction policy. -
Circular references with
__del__→ Remove__del__or break the cycle manually. -
File handles not closed → Wrap I/O in
withstatements.
Below is a concrete example that replaces a manual list‑based cache with an LRU cache:
# Before – naive cache that grows forever
_cache = []
def get_user(id):
for u in _cache:
if u.id == id:
return u
user = fetch_from_db(id)
_cache.append(user) # <-- memory leak in long‑running process
return user
# After – bounded LRU cache (fix applied)
from functools import lru_cache
@lru_cache(maxsize=1024)
def get_user(id):
return fetch_from_db(id)
💡 Ready‑to‑use patch: If you need a pre‑written script that automates the snapshot‑diff workflow, you can Download the pre‑configured script here. It bundles the tracemalloc boilerplate and prints a concise report.
6️⃣ Verify the Remedy
Rerun the load test with the patched code and repeat steps 2‑4. The RSS curve should now plateau.
python -m memory_profiler your_service.py
Look for a flat line after the initial warm‑up period.
Production‑Safe Deployment Tips
-
Feature‑flag the diagnostic code – Keep
tracemallocdisabled by default; enable via an env var (PYTHONTRACEMALLOC=1). - Log snapshot diffs to a rotating file rather than stdout.
- Automate alerts – Integrate the memory‑profile check into your CI/CD pipeline; fail the build if a new leak is introduced.
- Document the fix – Add a comment next to the changed section linking to the issue tracker and the commit that introduced the fix.
Conclusion
Memory leaks in Python are often a combination of lingering references and missing cleanup. By leveraging built‑in tools like tracemalloc, visual aids such as objgraph, and disciplined coding practices (bounded caches, context managers, and proper cycle handling), you can detect and eradicate leaks before they impact users.
For a complete end‑to‑end solution, including scripts, CI integration snippets, and a Docker‑ready environment, Get the complete patch tool or Access the full repository fix.
Top comments (0)