Hands-on gc.collect cost empty vs cyclic garbage lab: real collect latency plus reclaim counts, measured on Linux localhost today in this lab for SREs.
4 min read
Cost of gc.collect() when the heap is empty vs when it holds cyclic garbage. This lab reports collect latency, collects/s, and objects reclaimed on Linux localhost.
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. GC stayed enabled so cycles are tracked (disabled-GC setups under-count reclaim).
Verdict up front (50000 cycle pairs): empty full collect ~0.709 ms (~1411 collects/s); with cycles ~12.587 ms reclaiming ~100006 objects.
| Arm | Pattern |
|---|---|
| full collect empty | gc.collect() |
| gen0 empty | gc.collect(0) |
| full with cycles | build 2-cycles, then full collect |
| gen0 after cycles | often partial |
| full after gen0 | reclaim remainder |
| list alloc no cycles | allocation context |
Seven rounds, p50. Each with-cycles arm rebuilds garbage after a clean collect.
Script: lab-evidence/129-gc-collect-cost/results/run_lab.py.
| Arm | p50 ms | collects/s | collected |
|---|---|---|---|
| full empty | 0.709 | 1411 | 0 |
| gen0 empty | 0.0 | 3546080 | 0 |
| full + cycles | 12.587 | 79 | 100006 |
| gen0 after cycles | 0.155 | 6471 | 1964 |
| full after gen0 | 12.309 | 81 | 98042 |
After building cycles, gen0 reclaimed only ~1964 objects; the following full collect took ~12.309 ms and reclaimed ~98042. Calling gc.collect(0) in a request path can look “cheap” while leaving cyclic junk behind.
weakref, explicit clear) over hoping gen0 saves you.Building a plain 50000-int list (no cycles) ran at ~80686416 objs/s. That arm is not a collect — it shows allocation itself is far cheaper than a full collect over ~100006 cyclic objects.
A maintenance gc.collect() on a quiet process cost ~0.709 ms here. The same call after 50000 orphaned cycles jumped to ~12.587 ms. Put cycle-prone structures behind weakrefs or explicit teardown in long-lived workers; reserve full collects for controlled windows, not per-request cleanup.
collect() reclaims 0.gc.collect(0) as a “GC health check”.Evidence: summary.json, summary.txt.
One Linux box, CPython cyclic GC only. Not pymalloc arena tuning, not Go/Java collectors.
Manual collects belong in runbooks with a stated latency budget — not as a silent fix for “memory looked high” without graph evidence.
Empty gc.collect() ~0.709 ms; with 50000 cycle pairs ~12.587 ms reclaiming ~100006 objects. Prefer breaking cycles; do not trust gen0 alone for cyclic heaps.
Lab evidence
Ran the gc.collect benchmark on Linux localhost with Python 3.13.5, seven rounds at p50, keeping GC enabled. Empty full collect measured about 0.709 ms; 50,000 cycle pairs took about 12.587 ms and reclaimed about 100006 objects. gen0 reclaimed only about 1964 first, which confirmed that a full collect is needed for cyclic garbage.
Hands-on contextlib.ExitStack vs nested with and manual close: real cycles/s for N resources, measured on Linux localhost in this hands-on lab for SREs.
Hands-on1 Oct 20263 min read
Hands-on itertools.batched vs manual list-slice chunking: real items/s batching sequences, measured on Linux localhost today in this hands-on lab for SREs.
Hands-on1 Oct 20263 min read
Hands-on sqlite3 vs shelve local KV store lab: real insert/get ops/s plus file sizes, measured on Linux localhost today in this hands-on lab for SREs.
Hands-on1 Oct 20264 min read