15 February 2026 · 4 min
When programming, we need to allocate memory, and then deallocate it. If you program in C, you get used to malloc/free functions. Sadly, this leaves you vulnerable to memory leaks: unrecovered memory. Most popular programming languages today use automated memory management: Java, JavaScript, Python, C#, Go, Swift and so forth.
There are essentially two types of automated memory managements. The simplest method is reference counting. You track how many references there are to each object. When an object has no more references, then we can free the memory associated with it. Swift and Python use reference counting. The downside of reference counting are circular references. You may have your main program reference object A, then you add object B which references object A, and you make it so that object A also reference object B. Thus object B has one reference while object A has two references. If your main program drops its reference to object A, the both objects A and B still have a reference count of one. Yet they should be freed. To solve this problem, you could just visit all of your objects to detect which are unreachable, including A and B. However, it takes time to do so. Thus, the other popular approach of automated memory management: generational garbage collection. You use the fact that most memory gets released soon after allocation. Thus you track young objects and visit them from time to time. Then, more rarely, you do a full scan. The downside of generational garbage collection is that typical implementations stop the world to scan the memory. In many instances, your entire program is stopped. There are many variations on the implementation, with decades of research.
The common Python implementation has both types: reference counting and generational garbage collection. The generational garbage collection component can trigger pauses. A lot of servers are written in Python. It means that your service might just become unavailable for a time. We often call them ‘stop the world’ pauses. How long can this pause get?
To test this out, I wrote a Python function to create a classical linked list:
class Node:
def __init__(self, value):
self.value = value
self.next = None
def add_next(self, node):
self.next = node
def create_linked_list(limit):
""" create a linked list of length 'limit' """
head = Node(0)
current = head
for i in range(1, limit):
new_node = Node(i)
current.add_next(new_node)
current = new_node
return head
And then I create one large linked list and then, in a tight loop, we create small linked lists that are immediately discarded.
x = create_linked_list(50_000_000)
for i in range(1000000):
create_linked_list(1000)
A key characteristic of my code is the 50 million linked list. It does not get released until the end of the program, but the garbage collector may still examine it.
And I record the maximum delay between two iterations in the loop (using time.time()).
How bad can it get? The answer depends on the Python version. And it is not consistent from run-to-run. So I ran it once and picked whatever result I got. I express the delay in milliseconds.
| python version | system | max delay |
|---|---|---|
| 3.14 | macOS (Apple M4) | 320 ms |
| 3.12 | Linux (Intel Ice Lake) | 2,200 ms |
Almost all of this delay (say 320 ms) is due to the garbage collection. Creating a linked list with 1000 elements takes less than a millisecond.
How long is 320 ms? It is a third of a second, so it is long enough for human beings to notice it. For reference, a video game drawing the screen 60 times per second has less than 17 ms to draw the screen. The 2,200 ms delay could look like a server crash from the point of view of a user, and might definitely trigger a time-out (failed request).
I ported the Python program to Go. It is the same algorithm, but a direct comparison is likely unfair. Still, it gives us a reference.
| go version | system | max delay |
|---|---|---|
| 1.25 | macOS (Apple M4) | 50 ms |
| 1.25 | Linux (Intel Ice Lake) | 33 ms |
Thus Go has pauses that are several times shorter than Python, and there is no catastrophic 2-second pause.
Should these pauses be a concern? Most Python programs do not create so many objects in memory at the same time. Thus you are not likely to see these long pauses if you have a simple web app or a script. Python gives you a few options, such as gc.set_threshold and gc.freeze which could help you tune the behaviour.
Video
Daniel Lemire, "How bad can Python stop-the-world pauses get?," in Daniel Lemire's blog, February 15, 2026, https://lemire.me/blog/2026/02/15/how-bad-can-python-stop-the-world-pauses-get/.
[BibTeX]
I am interested in how “bad” the Stop-The-World pauses get if you were to compare it against the latest Python 3.14t, which is the “free-threaded” or “no-GIL” build of the Python 3.14 interpreter. Thank you, Dr. Lemire!
Instagram got a 10% speed up on their Django setup by disabling GC but it’s not because the GC itself is slow. It had some post-fork memory access patterns that wasted resources. Their writeup is here:
https://instagram-engineering.com/dismissing-python-garbage-collection-at-instagram-4dca40b29172
I’m being pedantic, but I can’t resist pointing out that Python-the-programming-language does not require reference counting. It’s CPython, the official reference implementation, that uses reference counting + garbage collection, but popular alternative implementations like PyPy and Jython use garbage collection only, and the Python language spec allows this.
This is actually an annoying source of incompatibility between implementations. For example, a loop like this:
for _ in range(1000000): open(‘/dev/null’)
will run just fine in CPython, but PyPy (usually) crashes with “Too many open files”. The reason is that the open file object, which is never closed explicitly in this example, is closed implicitly when it is garbage collected. With CPython, this deterministically happens after the open call returns, since the result is not assigned anywhere. With PyPy, garbage collection is deferred until memory is low, and systems usually run out of file descriptors long before they run out of memory, which causes the error.
The correct solution is not to rely on garbage collection to release resources other than heap memory, and always close files and other resource objects explicitly. The with-statement is useful to do this robustly, but beginning programmers often forget to use it, or don’t understand the need for it.
If it was up to me, I’d at least print a warning whenever an open file is garbage collected, since this is clearly a logic error. I don’t know why Python implementations don’t do this; perhaps they are afraid this will print too many spurious warnings for one-off scripts, where it’s arguably okay to open a handful of files that are never explicitly closed.
Thanks. I point out that, in the post, I wrote “The common Python implementation has both types (…)”. I consider CPython to be the common Python implementation.
I use reference counting only in the interpreter I wrote. There the answer to using circular data is (1) Don’t do it, and (2) If you do it, break the cycle yourself before abandoning the data.
Interesting… I don’t understand why the GC is involved at all, since the program produces no circular references…
Here’s what I thought would happen: After the call create_linked_list(1000) returns, the return value is discarded, which means its reference count goes to zero, so it is deallocated and the count of its outgoing reference is decremented, and so on, until the 1000 nodes have been deallocated. And I expected that all of this happens before execution actually returns to the main loop.
That should deallocate all 1000 nodes before the next iteration, memory should never run low, and each iteration should take pretty much the same amount of time.
In other words, I thought the whole reference counting machinery uses an eager approach: as soon as we find a zero counter, clear all references from this object, decrement their counters, check for zero, and so on recursively.
What am I missing? Does CPython delay some of the reference counting and deallocation until later? Or does a separate GC thread do the job? Interesting…
In other words, I thought the whole reference counting machinery uses an eager approach: as soon as we find a zero counter, clear all references from this object, decrement their counters, check for zero, and so on recursively.
Yes. I believe this is what happens.
But the generational garbage collector still runs periodically.
To be clear, the generational garbage collector is useless in my case, but it does not know that it is useless. So it has to do its work.
I definitely do an eager drop in my reference counting code:
https://github.com/chkoreff/Fexl/blob/master/src/value.c#L53
I did experiment with a lazy drop which simply queues up cells with refcount 0. It worked fine, but there’s always a possibility that large unused strings will get buried in the queue and never reclaimed on demand because new cells are constantly getting pushed on top of it. I played with methods of “nudging” the recycle queue so that all cells eventually get visited, but the whole thing just seemed needlessly complex so I went back to the eager approach. The eager approach was faster anyway.
I know this isn’t directly related to Python, but your question reminded me of my experience with reference counting implementations.