DEV Community

Deep Fix
Deep Fix

Posted on

How to Fix Python Memory Leaks in Production – Best Practices & Tools

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, werkzeug cache) 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

4️⃣ Pinpoint Hot Functions with memory_profiler

from memory_profiler import profile

@profile
def process_batch(batch):
    # heavy processing logic
    pass
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Fix

# fixed.py – bounded cache
from functools import lru_cache

@lru_cache(maxsize=1024)   # reasonable bound
def compute(value):
    return heavy_computation(value)
Enter fullscreen mode Exit fullscreen mode

Production‑Level Monitoring

  • Export process RSS via Prometheus (node_exporter or process-exporter).
  • Set alerts when RSS grows > 75 % of the container limit.
  • Periodically dump a tracemalloc snapshot 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)