Introduction
Memory leaks in long‑running Python services can silently degrade performance, increase latency, and eventually cause crashes. In production environments the impact is amplified because a single leak can affect thousands of requests per second. This guide walks you through the most common leak patterns, how to detect them, and concrete steps to eliminate them before they hit your users.
1. Typical Leak Sources
| Category | Example | Why it leaks |
|---|---|---|
| Reference cycles | obj_a = []; obj_b = []; obj_a.append(obj_b); obj_b.append(obj_a) |
CPython’s gc can break most cycles, but if objects define __del__ the collector skips them. |
| Unclosed resources | File handles, DB connections left open | The underlying OS descriptor stays allocated. |
| Caching without eviction | functools.lru_cache(maxsize=None) |
Unlimited growth when maxsize is omitted. |
| Third‑party extensions | C extensions that malloc without free
|
Python’s GC cannot see native allocations. |
2. Diagnosing a Leak in Production
2.1 Enable tracemalloc
import tracemalloc
tracemalloc.start()
# ... your application runs ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[Top 10 memory blocks]")
for stat in top_stats[:10]:
print(stat)
tracemalloc records every memory allocation performed by the Python interpreter. Compare snapshots taken at different times (e.g., after 1 h vs. 3 h) to spot growth.
2.2 Use objgraph to Find Orphaned Objects
import objgraph
objgraph.show_growth(limit=5)
# Visualize a specific type
objgraph.show_backrefs(objgraph.by_type('MyLeakyClass')[0], filename='leak.png')
objgraph prints the number of newly created objects and can generate a graph of reference chains, helping you pinpoint the exact code path that retains them.
2.3 Inspect Native Allocations
For C extensions, combine tracemalloc with valgrind or jemalloc statistics. In Docker you can expose /proc/<pid>/status to read VmRSS.
3. Step‑by‑Step Fixes
Step 1 – Break Reference Cycles
import gc
class Leaky:
def __init__(self):
self.self_ref = self # intentional cycle
def __del__(self):
pass # prevents GC from collecting the cycle
# Fix: avoid __del__ or use weakref
import weakref
class Fixed:
def __init__(self):
self.self_ref = weakref.ref(self)
If you must keep __del__, manually break the cycle before the object goes out of scope:
def cleanup(obj):
obj.self_ref = None
gc.collect()
Step 2 – Close Resources Promptly
# Bad
conn = db.connect()
# ... many code paths ...
# Missing conn.close()
# Good – context manager
with db.connect() as conn:
conn.execute("SELECT …")
Always prefer context managers (with) for files, sockets, DB connections, and thread pools.
Step 3 – Bound Caches
from functools import lru_cache
# Unlimited cache – dangerous in a worker process
@lru_cache(maxsize=None)
def compute(x):
return heavy_calculation(x)
# Fix – set a sensible limit and optionally clear
@lru_cache(maxsize=1024)
def compute(x):
return heavy_calculation(x)
# Periodic eviction (e.g., every 5 min)
compute.cache_clear()
Step 4 – Audit Third‑Party Extensions
Check the extension’s issue tracker for known leaks. If a patch exists, apply it or replace the library. For example, the requests library prior to version 2.25 leaked SSL sockets on rare edge cases.
4. Monitoring in Production
-
Prometheus exporter – expose
process_resident_memory_bytes. - Alert – fire when memory growth > 15 % over a 10‑minute window.
-
Automated heap dump – on alert, run a short script that captures a
tracemallocsnapshot and uploads it to S3 for post‑mortem analysis.
# prometheus.yml snippet
- job_name: 'python_app'
static_configs:
- targets: ['localhost:8000']
5. Real‑World Example
Below is a minimal reproduction of a leak caused by a global list that accumulates request IDs:
# leaky_app.py
import uuid
leaked_ids = []
def handle_request():
request_id = uuid.uuid4()
leaked_ids.append(request_id) # <- never cleared
return "OK"
Fix – store IDs in a bounded deque:
from collections import deque
leaked_ids = deque(maxlen=1000) # keep only the newest 1k IDs
Deploy the fix and watch the memory graph flatten.
6. Take Action Now
Ready to harden your services? Download the pre‑configured script here to automatically instrument your Python processes with tracemalloc and generate periodic reports. For a full‑featured patch kit, Get the complete patch tool that includes cache‑size enforcement and resource‑wrapper decorators. Need the source? Access the full repository fix and adapt it to your codebase.
Conclusion
Memory leaks are rarely “magical” – they stem from predictable patterns like unclosed resources, unbounded caches, or reference cycles. By equipping your production environment with lightweight diagnostics (tracemalloc, objgraph), enforcing disciplined resource handling, and monitoring memory trends, you can eradicate leaks before they affect customers.
Stay vigilant, instrument early, and keep your Python services humming at peak performance.
Top comments (0)