Generational Garbage Collection
CPython’s cycle collector is generational: it sorts the container objects it tracks into three age buckets — generation 0 (young), generation 1 (middle), and generation 2 (old) — and collects the young bucket frequently and cheaply while touching the old bucket only rarely. The design rests on the generational hypothesis: most objects die young, so concentrating collection effort on recently-allocated objects reclaims most garbage for the least work. This note describes the generational collector as it exists in CPython 3.14.5 (released 10 May 2026). That is a deliberately precise version statement: the incremental collector that shipped in 3.14.0 was reverted in 3.14.5 back to the three-generation design described here — see The Incremental GC and Its Reversion for that story. The generational machinery sits on top of the cycle-detection algorithm described in The Cyclic Garbage Collector; this note is about when and how often that algorithm runs, not the algorithm itself.
The default generation-0 threshold is 2000, not the historical 700 of CPython 3.12 and earlier. The value was raised to 2000 as part of the incremental-collector work in gh-108362 (“Incremental Cycle GC”, PR GH-116206, merged 2024-03-20): that commit’s diff to pycore_runtime_init.h rewrites { .threshold = 700, } to .young = { .threshold = 2000, }. When the generational collector was forward-ported for 3.14.5 (PR GH-148746, 2026-04-30), the 2000 value was carried over into the restored three-generation GC_GENERATION_INIT for the GIL build. Notably the 2000 value outlived the incremental structure it arrived with: the incremental collector was reverted on the 3.13 branch before 3.13.0 final (see The Incremental GC and Its Reversion), yet 3.13.0 final still ships generations[0].threshold = 2000. So 2000 first shipped in 3.13.0 and persists through 3.14.5, verified by diffing GC_GENERATION_INIT across the v3.12.0 (700), v3.13.0, v3.13.5, and v3.14.5 (2000) tags. The official gc docs give no numeric default, so this is a source-grounded fact, not a docs claim.
Resolved (2026-06-01)
Default gen-0 threshold is 2000 (was 700 through 3.12). The 700→2000 change landed with the incremental GC (gh-108362, PR GH-116206) and was kept by the 3.14.5 generational forward-port (PR GH-148746). Confirmed by per-tag
GC_GENERATION_INITdiffs v3.12.0 → v3.14.5.
The Generational Hypothesis
The collector exists for one reason: reference counting cannot reclaim cycles. When object A holds a reference to object B and B holds a reference back to A, each keeps the other’s reference count above zero forever, even after the program drops all external references. Reference counting (see Reference Counting in CPython) frees an object the instant its count hits zero, but it never sees zero for objects trapped in a cycle. The cyclic garbage collector is the second-tier mechanism that finds and frees exactly those unreachable cycles (The Cyclic Garbage Collector).
Running that cycle-detection scan is expensive — it must visit every container object the interpreter tracks and traverse all their references. Running it on every allocation would be catastrophically slow. The generational design is the optimization that makes it affordable, and it is built on an empirical observation known as the generational hypothesis (or “weak generational hypothesis”): the vast majority of objects die young. A list comprehension’s temporary list, a function’s local dictionary, the tuple returned and immediately unpacked — these are born, used, and discarded within microseconds. A relatively small population of objects (module globals, caches, long-lived data structures) survives for the life of the program.
If most garbage is young, then the cheapest way to reclaim most garbage is to scan only the young objects, and scan them often. Old objects are scanned rarely, because experience says they are unlikely to have become garbage since the last time we looked. CPython operationalizes this with three generations and a set of thresholds that bias collection effort heavily toward the youngest.
Mental Model
flowchart TD A["New container object<br/>(list, dict, instance, tuple…)"] -->|"_PyObject_GC_Link()<br/>gen-0 count++"| G0["Generation 0 (young)<br/>threshold = 2000"] G0 -->|"gen-0 count > 2000<br/>→ collect gen 0"| C0{"Survives<br/>collection?"} C0 -->|"unreachable cycle"| F0["Freed"] C0 -->|"reachable → promote"| G1["Generation 1 (middle)<br/>threshold = 10"] G1 -->|"gen-1 count > 10<br/>→ collect gen 0+1"| C1{"Survives?"} C1 -->|"unreachable"| F1["Freed"] C1 -->|"reachable → promote"| G2["Generation 2 (old)<br/>threshold = 10<br/>+ 25% long-lived guard"] G2 -->|"gen-2 count > 10<br/>→ full collection"| C2{"Survives?"} C2 -->|"unreachable"| F2["Freed"] C2 -->|"reachable → stays in gen 2"| G2
Figure: An object’s lifecycle through the three generations. The insight to extract is the funnel shape: nearly everything enters at generation 0 and most is freed there; only objects that survive a young collection are promoted to the middle generation, and only middle-survivors reach the old generation. Each promotion makes an object cheaper to re-examine (it is scanned less often) but more expensive to eventually collect (the old generation is large). The thresholds 2000 / 10 / 10 mean very different things: 2000 counts net allocations, while the two 10s count how many times the younger generation was collected.
Mechanical Walk-through
The three generations and what they hold
The number of generations is fixed at compile time: #define NUM_GENERATIONS 3 in pycore_interp_structs.h (per the 3.14.5 source). Each generation is a circular doubly-linked list of PyGC_Head nodes — every GC-tracked object carries a small header (_gc_next, _gc_prev) that threads it onto exactly one generation’s list. Note the qualifier tracked: only container types (lists, dicts, sets, tuples, class instances, and other objects whose type sets Py_TPFLAGS_HAVE_GC) participate. Atomic objects like int, float, and str are never tracked because they cannot form cycles — they hold no references to other Python objects.
A freshly allocated tracked object is linked onto generation 0. The relevant code increments the young generation’s counter on every such link (gcstate->generations[0].count++; /* number of allocated GC objects */, gc.c:1865).
The thresholds and what each number means
The default thresholds are a 3-tuple. In the GIL build the static initializer reads:
#define GC_GENERATION_INIT \
.generations = { \
{ .threshold = 2000, }, \
{ .threshold = 10, }, \
{ .threshold = 10, }, \
}, \(from pycore_interp_structs.h, wired into runtime startup at pycore_runtime_init.h:140 via GC_GENERATION_INIT). The three numbers do not mean the same thing — this is the single most misunderstood fact about the collector, and the official set_threshold documentation spells it out (docs.python.org/3.14/library/gc.html):
“In order to decide when to run, the collector keeps track of the number of object allocations and deallocations since the last collection. When the number of allocations minus the number of deallocations exceeds threshold0, collection starts. Initially only generation 0 is examined. If generation 0 has been examined more than threshold1 times since generation 1 has been examined, then generation 1 is examined as well.”
Reading the meaning out of that:
- threshold0 = 2000 is an allocation count. When
(allocations − deallocations)for tracked objects since the last gen-0 collection exceeds 2000, a generation-0 collection is scheduled. This is the only threshold compared against a count of objects. - threshold1 = 10 is a collection-count ratio. It is not a count of objects in generation 1 — it is the number of times generation 0 must have been collected since the last time generation 1 was collected. After ~10 gen-0 collections, the next collection sweeps generations 0 and 1 together.
- threshold2 = 10 is the analogous ratio for the old generation: after generation 1 has been collected ~10 times, the next collection becomes a full collection of generations 0, 1, and 2.
So the effective cadence is roughly: a young collection every 2000 net allocations; a middle collection every ~10 young collections; a full collection every ~10 middle collections (≈ every 100 young collections), modulo the long-lived guard described below.
Version scope
This describes 3.14.5. In 3.14.0–3.14.4 (the incremental collector)
threshold2was ignored andgc.collect(1)performed an increment rather than a generation collection; 3.14.5 restoredthreshold2and the three-generationcountmeaning. Verified against the live gc docs “Changed in version 3.14” / “Changed in version 3.14.5” notes forset_thresholdandcollect. Do not apply this note verbatim to a 3.14.0–3.14.4 interpreter. See The Incremental GC and Its Reversion.
How the trigger actually fires
On each allocation of a tracked object, after incrementing generations[0].count, the linker checks whether a collection should run (gc.c:1866):
gcstate->generations[0].count++; /* number of allocated GC objects */
if (gcstate->generations[0].count > gcstate->generations[0].threshold &&
gcstate->enabled &&
gcstate->generations[0].threshold &&
!_Py_atomic_load_int_relaxed(&gcstate->collecting) &&
!_PyErr_Occurred(tstate))
{
_Py_ScheduleGC(tstate);
}- Line 1: bump the young-generation allocation counter.
- Line 2: the count exceeded the threshold (2000).
- Line 3:
gcstate->enabled— collection has not been turned off via [[GC Thresholds and Tuning|gc.disable()]]. - Line 4:
gcstate->generations[0].thresholdis nonzero — setting threshold0 to 0 disables automatic collection entirely. - Line 5: a collection is not already in progress (
collectingflag). - Line 6: no exception is pending (collection runs at a safe point, not mid-exception).
_Py_ScheduleGC does not collect immediately — it sets the “eval breaker” flag so the collection happens at the next safe point in the bytecode loop, not in the middle of object construction.
Choosing which generation to collect
When the scheduled collection runs, gc_select_generation picks the oldest generation whose count exceeds its threshold, and collects that generation and all younger ones (gc.c:1257):
for (int i = NUM_GENERATIONS-1; i >= 0; i--) {
if (gcstate->generations[i].count > gcstate->generations[i].threshold) {
...
return i;
}
}It walks from oldest (index 2) to youngest (index 0). The first generation whose count > threshold is the one collected, together with every younger generation merged into it. This is why a gen-1 collection always includes gen-0, and a full collection includes all three.
Promotion: how an object ages
After the cycle-detection scan partitions a generation’s objects into reachable and unreachable, the unreachable ones (the cyclic garbage) are finalized and freed, and the survivors are promoted by being merged into the next-older generation’s list (gc.c around 1373–1405):
/* update collection and allocation counters */
if (generation+1 < NUM_GENERATIONS) {
gcstate->generations[generation+1].count += 1;
}
for (i = 0; i <= generation; i++) {
gcstate->generations[i].count = 0;
}
/* merge younger generations with one we are currently collecting */
for (i = 0; i < generation; i++) {
gc_list_merge(GEN_HEAD(gcstate, i), GEN_HEAD(gcstate, generation));
}
...
/* Move reachable objects to next generation. */
if (young != old) {
if (generation == NUM_GENERATIONS - 2) {
gcstate->long_lived_pending += gc_list_size(young);
}
gc_list_merge(young, old);
}This is the precise implementation of the “threshold1/2 are collection-count ratios” rule:
generations[generation+1].count += 1— collecting generation g increments the next generation’s counter by one. So generation 1’scountis literally “number of gen-0 collections since gen-1 was last collected,” and generation 2’scountis “number of gen-1 collections since gen-2 was last collected.” That is what makes thresholds 1 and 2 ratios rather than object counts.generations[i].count = 0fori <= generation— every collected generation’s counter resets to zero.gc_list_merge(young, old)— the survivors of the collected generation are spliced onto the next-older generation’s list. An object that survives a gen-0 collection lands in gen-1; survive a gen-1 collection and it lands in gen-2; in gen-2 (young == old), survivors simply stay there. An object’s “generation” is just which list it currently lives on, and promotion is a list-splice — no copying, no relocation. (This is a non-moving collector; objects never change address.)
Why a young-gen collection is cheap
A generation-0-only collection scans only the objects currently on the gen-0 list — at most roughly threshold0 objects (2000), because that is how many net allocations triggered it. The cost of a young collection is therefore bounded by a constant, independent of how much long-lived data the program holds. The comment in gc_select_generation states this directly: “non-full collections (i.e., collections of the young and middle generations) will always examine roughly the same number of objects — determined by the aforementioned thresholds.” Because most objects die young, this small, cheap scan reclaims the bulk of cyclic garbage. That is the entire payoff of the generational design.
The full-collection cost and the 25% long-lived guard
A full collection (generation 2) is the expensive case: its cost is proportional to the total number of long-lived objects, which is unbounded. If a program builds a large list of GC-tracked objects and the collector ran a full collection every fixed number of allocations, the total work would be quadratic in the number of objects (each of the n full collections re-scans all O(n) survivors). This is the real performance bug recorded as CPython issue #4074.
The fix, proposed by Martin von Löwis on python-dev in June 2008 (python-dev archive, cited in the gc.c comment), is the long-lived ratio guard. Even when gen-2’s count exceeds its threshold, a full collection is skipped unless (gc.c:1298):
if (i == NUM_GENERATIONS - 1
&& gcstate->long_lived_pending < gcstate->long_lived_total / 4)
{
continue; // skip the full collection this time
}long_lived_pending is the number of objects promoted into gen-2 since the last full collection; long_lived_total is the total number of long-lived objects. A full collection runs only when pending ≥ 25% of total (/ 4 in the code). Von Löwis’s original 2008 proposal phrased it as “the number of survivor objects from the middle generation must exceed 10% of the number of objects in the oldest generation,” and his simulation showed this turns quadratic full-collection cost into roughly O(N) amortized overhead, with the allocated-to-inspected ratio stabilizing around 1:10 at scale (python-dev, June 2008). The constant tightened to 25% in the shipped implementation. The gc.c comment summarizes the effect memorably: “each full garbage collection is more and more costly as the number of objects grows, but we do fewer and fewer of them” — yielding amortized linear total cost in the number of objects.
Common Misunderstandings
“threshold = 700 in modern Python.” No — 700 was the default through 3.12. Since 3.13.0 it is 2000. This was verified by tag-diff:
| CPython tag | gen-0 default threshold |
|---|---|
v3.12.0 | 700 ({ .threshold = 700, }) |
v3.13.0 | 2000 |
v3.13.5 | 2000 |
v3.14.5 | 2000 |
(All read from GC_GENERATION_INIT in pycore_runtime_init.h / pycore_interp_structs.h at each tag.) Any text — including older docs, blog posts, and the task brief — that says “700” is describing pre-3.13 behavior. Check at runtime with gc.get_threshold().
“threshold1 = 10 means gen-1 collects after 10 objects.” No. It collects after gen-0 has been collected ~10 times. Generation 1 can hold far more than 10 objects.
“Collecting generation 2 only scans generation 2.” No. Collecting any generation collects it and all younger generations — they are merged in first. There is no way to scan the old generation in isolation.
“GC frees memory the moment an object becomes garbage.” That is reference counting, not the generational collector. The generational collector only handles cycles, and only runs at the threshold-driven cadence above. Most memory is freed immediately by refcounting; the collector is a periodic mop-up for the cyclic remainder.
Tuning and Operation
The cadence described here is fully observable and adjustable at runtime through the gc module — gc.get_threshold(), gc.set_threshold(), gc.get_count(), gc.collect(generation), and the freeze/disable techniques for fork-heavy servers. Those operating knobs and their trade-offs are the subject of the sibling note GC Thresholds and Tuning; the full API surface is catalogued in The gc Module.
See Also
- The Cyclic Garbage Collector — the cycle-detection algorithm (gc_refs / tp_traverse) that the generational scheduler invokes.
- GC Thresholds and Tuning — sibling: operating and tuning the collector via the
gcmodule. - The Incremental GC and Its Reversion — the 3.14.0 incremental collector that was reverted in 3.14.5 back to this generational design.
- Reference Counting in CPython — the primary memory reclamation mechanism; the collector exists only to handle what refcounting cannot (cycles).
- Reference Cycles and How They Form — why cycles arise and why refcounting leaks them.
- The gc Module — full API reference.
- Python Internals MOC — §7 Garbage Collection.