Find Python Memory Leaks: Practical Guide

Published on: May 28, 2026
Reading time: 9 minutes
Detecção de vazamento de memória em aplicações Python

Python manages memory automatically, but that does not mean memory leaks are impossible. Long-running scripts, APIs, workers, crawlers, data pipelines, and desktop applications can slowly consume more RAM until performance drops or the operating system kills the process. Learning how to find Python memory leaks is essential when an application starts fast but becomes unstable after hours or days of execution.

This English version is adapted for readers who want a practical debugging workflow, not a literal translation. You will learn what a memory leak means in Python, why reference counting and garbage collection do not solve every case, which symptoms to watch, how to use tracemalloc, how to inspect the garbage collector with gc, how to compare snapshots, and how to fix common causes such as unbounded caches, global lists, circular references, and forgotten resources. If you are still building your debugging foundation, read this guide to debugging Python with pdb and this article on finding Python bottlenecks with cProfile.

What Is a Memory Leak in Python?

A memory leak happens when a program keeps memory alive even though that memory is no longer useful. In Python, this usually does not mean that Python forgot how to free memory. More often, it means your program still has references to objects, so Python correctly assumes those objects are still needed. The result is the same from the user’s point of view: RAM usage keeps growing and does not return to a stable baseline.

The official Python tracemalloc documentation explains how to trace memory blocks allocated by Python. That tool is often the best first step because it can show which lines allocate the most memory. The official gc module documentation is also useful when you need to inspect Python’s garbage collector and circular references.

Why Memory Leaks Still Happen in Python

Python mainly uses reference counting plus a cyclic garbage collector. When an object has no references, it can be reclaimed. When objects reference each other in cycles, the garbage collector can often detect and clean them. However, memory can still grow if your application stores references in global containers, caches, queues, session dictionaries, callback lists, log buffers, or long-lived objects that never get cleared.

Some apparent leaks are not classic leaks. Python’s memory allocator may keep memory reserved for reuse instead of immediately returning it to the operating system. A process may look larger in system tools even after objects are freed. That is why you need internal allocation snapshots and application-level measurements instead of relying only on Task Manager, Activity Monitor, top, or container memory graphs.

Common Symptoms of a Python Memory Leak

The most obvious symptom is a process whose memory usage grows steadily over time. A web API may handle requests normally at first and then slow down after thousands of calls. A worker may consume more memory after each job. A data pipeline may never return to its previous memory baseline after processing a batch. Eventually, the process may raise MemoryError or be killed by the operating system.

Other signs include increasing garbage collection pressure, slower response times, larger queues, container restarts, swap usage, and logs showing out-of-memory events. If your program crashes with allocation failures, this guide to solving MemoryError in Python can help with the immediate failure, but a leak investigation should focus on why memory grows in the first place.

Start with a Reproducible Scenario

Before using tools, create a scenario that reproduces the growth. For an API, run the same endpoint many times. For a worker, process a representative batch repeatedly. For a script, isolate the loop that appears to grow memory. A memory leak that cannot be reproduced is much harder to diagnose because every snapshot may show a different workload.

Keep the test narrow. Disable unrelated features when possible. Use a fixed input dataset. Record memory before and after each iteration. This makes it easier to separate real growth from normal workload variation. A small reproducible script is often more valuable than a complex production dump with many moving parts.

Use tracemalloc to Compare Snapshots

tracemalloc is included in the standard library and can trace memory allocations made by Python. The most useful workflow is not only taking one snapshot, but comparing two snapshots: one before the suspicious workload and one after it. The difference shows which lines allocated more memory between those points.

import tracemalloc

tracemalloc.start()

snapshot_before = tracemalloc.take_snapshot()

# Run the suspicious workload here
items = []
for number in range(100_000):
    items.append({"number": number, "square": number * number})

snapshot_after = tracemalloc.take_snapshot()

for stat in snapshot_after.compare_to(snapshot_before, "lineno")[:10]:
    print(stat)

This output points to file names and line numbers where allocations increased. The result does not automatically prove a leak, but it tells you where memory is growing. If the same lines keep accumulating after repeated workloads, you have a strong lead.

Filter Noise in tracemalloc Results

Real applications allocate memory in frameworks, dependencies, and the standard library. To focus on your code, you can filter traces by file path. This makes the report easier to read and reduces noise from import machinery or third-party packages.

import tracemalloc

tracemalloc.start()
snapshot = tracemalloc.take_snapshot()

filtered = snapshot.filter_traces((
    tracemalloc.Filter(True, "*/my_project/*"),
))

for stat in filtered.statistics("lineno")[:10]:
    print(stat)

Use filtering after you understand the broad picture. Sometimes the leak is triggered by your code but allocated inside a library. In that case, filtering too aggressively can hide the signal. Start broad, then narrow the scope.

Inspect Garbage Collection with gc

The gc module lets you interact with Python’s cyclic garbage collector. It can force a collection, report object counts, and expose objects that could not be collected. This helps when you suspect circular references or objects kept alive longer than expected.

import gc

collected = gc.collect()
print("Collected objects:", collected)
print("Uncollectable objects:", gc.garbage)

For many applications, gc.collect() returning a number is not enough. You need to identify why objects are still reachable. If a global list, cache, closure, or object graph still points to them, the garbage collector will not free them because they are still considered alive.

Common Cause: Unbounded Lists and Dictionaries

The simplest memory leak is an unbounded container. A global list that stores every request, every parsed row, every message, or every error will grow forever. Python is not leaking in the low-level sense; your application is intentionally keeping references. The fix is to limit, clear, aggregate, stream, or persist data instead of holding everything in memory.

# Dangerous in a long-running process
history = []

def handle_event(event):
    history.append(event)

A safer design may use a bounded queue, a database, a log file, or a rolling buffer. If you only need the last 1000 events, do not keep a list of every event since startup. If you are working heavily with containers, review Python lists and Python dictionaries.

Common Cause: Caches Without Limits

Caches are useful, but an unlimited cache can become a leak. If keys are highly variable, the cache may grow forever. This can happen with user-specific data, URLs, search queries, API responses, or dynamically generated objects. The solution is to use size limits, expiration, invalidation, or a cache backend designed for production.

from functools import lru_cache

@lru_cache(maxsize=1024)
def expensive_lookup(key):
    return compute_value(key)

A bounded lru_cache is safer than an unbounded dictionary. However, even bounded caches should be monitored if values are large. Cache design is a trade-off between speed and memory. If performance is the goal, read this guide on why Python can be slow and how optimization decisions affect resource usage.

Common Cause: Circular References

Circular references happen when objects refer to each other. Python’s cyclic garbage collector can usually handle them, but cycles involving finalizers, external resources, or complex object graphs can still cause problems or delays. Circular references are common in parent-child trees, callbacks, observers, and bidirectional relationships.

class Node:
    def __init__(self, name):
        self.name = name
        self.parent = None
        self.children = []

parent = Node("parent")
child = Node("child")
parent.children.append(child)
child.parent = parent

This pattern is not always wrong. But if nodes are removed from the application and still referenced somewhere, memory can grow. Consider weak references, explicit cleanup, or simpler ownership rules when object graphs become difficult to reason about.

Common Cause: Open Resources

Files, sockets, database connections, HTTP responses, and image objects can hold memory or operating system resources. Always close resources when you are done. The safest pattern is a context manager with with, because it closes the resource even if an exception occurs.

with open("data.txt", "r", encoding="utf-8") as file:
    content = file.read()

For network clients, database sessions, and framework-specific objects, follow the library’s lifecycle rules. Many production memory issues are caused not by Python lists, but by sessions, cursors, connections, or response objects that stay open longer than expected.

Use Generators for Large Data

If your code loads a full dataset when it only needs one item at a time, memory will spike. Generators let you stream values lazily. This does not fix every leak, but it reduces memory pressure and makes accidental accumulation easier to spot. A generator is useful for processing files, batches, API pages, and transformation pipelines.

def read_lines(path):
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            yield line.strip()

Generators are especially helpful when you combine them with bounded queues and batch processing. This guide to creating efficient generators with yield explains the technique in more detail.

A Practical Investigation Workflow

Start by confirming memory growth with a repeatable workload. Then add tracemalloc snapshots before and after the workload. Compare snapshots by line number. Check whether the same lines grow after each repetition. Inspect containers, caches, queues, and object graphs near those lines. Force garbage collection only as a diagnostic step, not as the main fix. Finally, rewrite the code so unused objects are no longer referenced.

For web services and workers, run the test in an environment similar to production. Some leaks appear only with real middleware, connection pools, serializers, or background tasks. If your application runs in containers, watch both process memory and container limits. A container restart may hide the problem temporarily, but it does not fix the leak.

What Not to Do

Do not fix a memory leak by blindly calling gc.collect() after every request or loop. That may reduce symptoms temporarily, but it does not remove the references keeping objects alive. Do not assume every increase in system memory is a leak. Python and the operating system may reuse allocated memory. Do not optimize randomly before identifying which objects are growing.

Also avoid deleting variables everywhere without understanding ownership. A local variable disappears when the function returns unless something else still references the object. The real question is not “how do I delete this variable?” but “who still references this object, and should they?”

Final Checklist

Confirm that memory grows repeatedly. Reproduce the issue with a controlled workload. Use tracemalloc snapshots to find growing allocation lines. Use gc to inspect garbage collection behavior when cycles are suspected. Check global containers, caches, queues, callbacks, sessions, and open resources. Prefer bounded data structures, context managers, streaming, and explicit lifecycle management.

Python memory leaks are usually caused by references that live longer than intended. Once you identify who still holds those references, the fix becomes much clearer. With tracemalloc, gc, careful reproduction, and disciplined resource management, you can keep long-running Python applications stable and predictable.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Error Resolution
    Foto de perfil de Leandro Hirt da Academify

    Python Enums: Avoid Magic Values and Bugs

    Learn how Python enums work, when to use Enum classes, how they prevent magic values, and how to write safer,

    Ler mais

    Tempo de leitura: 9 minutos
    28/05/2026
    Erro Python not recognized no terminal do Windows
    Error Resolution
    Foto de perfil de Leandro Hirt da Academify

    Fix ‘Python is not recognized’ Error on Windows

    Learn why Windows shows 'Python is not recognized' and how to fix it by adding Python to PATH, configuring environment

    Ler mais

    Tempo de leitura: 8 minutos
    28/05/2026
    Detecção de vazamento de memória em aplicações Python
    Error Resolution
    Foto de perfil de Leandro Hirt da Academify

    Find Python Memory Leaks: Practical Guide

    Learn how to find Python memory leaks with tracemalloc, gc, memory profilers, snapshots, and safe fixes for long-running apps.

    Ler mais

    Tempo de leitura: 9 minutos
    26/05/2026
    Debug de código Python usando o módulo pdb
    Error Resolution
    Foto de perfil de Leandro Hirt da Academify

    Debug Python with pdb: Beginner Guide

    Learn how to debug Python with pdb, use breakpoints, inspect variables, step through code, handle loops, and fix bugs faster.

    Ler mais

    Tempo de leitura: 10 minutos
    26/05/2026
    Uso do cProfile para identificar gargalos em código Python
    Error Resolution
    Foto de perfil de Leandro Hirt da Academify

    Find Python Bottlenecks with cProfile

    Learn how to use Python cProfile to find performance bottlenecks, read profiling reports, sort results with pstats, and optimize code

    Ler mais

    Tempo de leitura: 10 minutos
    21/05/2026