How to Fix Python Memory Leaks in Production
Keywords: Python memory leak, fix, production, debugging, monitoring
Introduction
Memory leaks in a Python service can silently degrade performance, cause out‑of‑memory (OOM) crashes, and increase cloud costs. In a production environment you need a repeatable, low‑overhead workflow to detect, diagnose, and resolve leaks before they affect users.
Common Causes
-
Reference cycles involving objects that implement
__del__. - Unclosed file/network handles that keep buffers alive.
-
Caching libraries (
functools.lru_cache,django cache, etc.) that grow without bounds. - Native extensions that allocate memory outside the Python heap.
- Global mutable state that accumulates data over time.
Step‑by‑Step Troubleshooting
1. Baseline Memory Usage
import psutil, os, time
process = psutil.Process(os.getpid())
print(f"RSS start: {process.memory_info().rss / 1024**2:.2f} MB")
Run this at service start and after a typical load cycle to establish a baseline.
2. Enable tracemalloc
import tracemalloc
tracemalloc.start()
# Run the part of the code you suspect
snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics('lineno')
print('Top memory allocations:')
for stat in top[:10]:
print(stat)
tracemalloc shows where Python objects are allocated, helping you pinpoint hot spots.
3. Use memory_profiler for line‑by‑line insight
pip install memory_profiler
from memory_profiler import profile
@profile
def heavy_job():
data = []
for i in range(1000000):
data.append({'id': i, 'value': 'x'*100})
return data
heavy_job()
The output highlights the exact line that spikes memory.
4. Detect Reference Cycles with gc
import gc, sys
gc.set_debug(gc.DEBUG_LEAK)
# ... run workload ...
for obj in gc.get_objects():
if sys.getrefcount(obj) > 2:
print(type(obj), sys.getrefcount(obj))
If you see unexpected high reference counts, break the cycle manually or use weakref.
5. Profile Native Extensions
If you use C‑extensions (e.g., numpy, pandas), employ Valgrind or AddressSanitizer in a staging build to catch leaks outside the Python heap.
Fix Patterns
| Symptom | Typical Fix |
|---|---|
| Growing list/dict never cleared | Explicit clear() after use or switch to a deque with a max length. |
| Unbounded LRU cache | Set maxsize or use cachetools.TTLCache. |
| Open file handles | Use with open(...) as f: or ensure close() in finally. |
Reference cycle with __del__
|
Remove __del__ or replace with context manager (__enter__/__exit__). |
| Native memory not freed | Call library‑specific free() functions or upgrade to a version with fixed leaks. |
Automation & Monitoring
- Prometheus exporter for RSS/VMEM:
from prometheus_client import Gauge, start_http_server
import psutil, os, time
mem_gauge = Gauge('process_resident_memory_bytes', 'Resident memory size')
def record_memory():
process = psutil.Process(os.getpid())
mem_gauge.set(process.memory_info().rss)
if __name__ == '__main__':
start_http_server(8000)
while True:
record_memory()
time.sleep(30)
Set an alert when the metric grows beyond a threshold.
-
CI lint – add a nightly job that runs a short workload with
tracemallocand fails if the top‑10 allocation list changes dramatically.
Real‑World Example
A Flask microservice started leaking ~150 MB every hour because a global cache grew without eviction.
# before – problematic global cache
CACHE = {}
def get_user(uid):
if uid in CACHE:
return CACHE[uid]
user = db.fetch(uid)
CACHE[uid] = user # never expires
return user
Fix – replace with cachetools.LRUCache:
from cachetools import LRUCache
CACHE = LRUCache(maxsize=10_000) # evicts oldest entries
After deployment, the memory profile flattened, and the OOM alerts disappeared.
Wrap‑Up
- Start with a baseline and monitor continuously.
- Use built‑in tools (
tracemalloc,gc,memory_profiler) for fast iteration. - Break reference cycles and limit unbounded collections.
- Automate detection in CI/CD pipelines and expose metrics for runtime alerts.
Ready to accelerate your fix? Download the pre‑configured script here, or explore the full repository for an end‑to‑end patch solution – Get the complete patch tool, and Access the full repository fix.
Top comments (0)