Memory Fragmentation in Long-Running Python Processes

Long-running Python applications often suffer from high memory consumption where the resident set size (RSS) grows continually even when the application is not leaking references. This article explains the architectural mechanisms behind memory fragmentation in CPython, focusing on how internal memory managers like PyMalloc operate, why memory is rarely returned to the operating system, and the primary coding patterns that induce fragmentation.

Python’s Internal Memory Architecture

To understand fragmentation, one must first look at how CPython manages memory through its three-tier abstraction:

Small allocations (512 bytes or fewer) are handled directly by Python's internal allocator, pymalloc. Large allocations bypass pymalloc and are delegated directly to the underlying system allocator (glibc malloc on most Linux systems).

1. The Arena Pinning Problem

The primary structural cause of fragmentation in Python is arena pinning. An arena can only be returned to the operating system if every single pool and block within that 256 KB boundary is completely empty.

If a long-running process allocates hundreds of arenas during a traffic spike, millions of objects are created. When the spike ends, the garbage collector frees the majority of these objects. However, if even a single tiny object—such as an 8-byte integer or a small string—remains referenced inside a 256 KB arena, the entire arena remains allocated in the process's resident memory. Over time, memory resembles a slice of Swiss cheese: predominantly free space internally, but completely un-reclaimable by the OS.

2. Interleaved Lifecycles of Heterogeneous Objects

Fragmentation accelerates when short-lived and long-lived objects are allocated concurrently.

Consider a web worker handling HTTP requests:

Because these allocations happen in parallel, short-lived and long-lived objects end up residing in the same memory pools and arenas. Once the request lifecycle finishes, the short-lived blocks are freed for reuse by Python, but the interspersed long-lived objects hold the arenas hostage.

3. System Allocator (glibc) Limitations

For objects exceeding 512 bytes (such as large strings, byte buffers, NumPy arrays, or large dictionaries), Python uses the system malloc.

The default GNU C Library allocator (glibc malloc) manages memory heaps using a combination of brk (for adjusting the data segment break point) and mmap (for large anonymous allocations). With brk, memory must be released sequentially from the top of the heap down. If an allocation at the top of the heap remains active, no memory below it can be trimmed back to the OS kernel, creating top-of-heap fragmentation at the C library level.

4. Over-Allocation in Dynamic Collections

Python dictionaries, lists, and sets employ aggressive over-allocation strategies to achieve \(O(1)\) amortized insertion:

When these collections shrink (e.g., items are removed or cleared), Python does not automatically shrink the underlying allocated buffer proportionally. A dictionary that once held 500,000 items continues to hold a large allocated hash table even if it is reduced to only 5 items. Unless explicitly reassigned to a new instance, these structures consume fragmented, empty space.

5. Cyclic References and Garbage Collection Delays

CPython primarily uses reference counting for memory deallocation. When an object's reference count drops to zero, its memory block is immediately marked as free within its pool.

However, cyclic references (objects referencing each other) cannot be resolved by reference counting alone. They must wait for the cyclic Garbage Collector (GC) to run across Generation 0, 1, or 2. During the delay between reference isolation and a cyclic GC cycle, cyclic structures pin their surrounding memory pools, preventing adjacent spaces from consolidating and amplifying fragmentation under steady throughput.

Mitigating Memory Fragmentation

While fragmentation cannot be completely eliminated in dynamic languages, specific techniques minimize its impact on long-running processes: