When is calling gc.disable() in a production Python service defensible, and what breaks?
answer
- It does not turn off memory management
- Counting keeps working; cycles do not
- Short-lived processes pay no price
- Before a fork, freeze then collect
- If you disable, own the collect call
basics
~20 sIt stops automatic cycle collection only — reference counting keeps freeing everything acyclic — so it is defensible in short-lived or fork-heavy processes. What breaks is cyclic garbage: it accumulates without bound unless the code calls gc.collect() at safe points.
solid answer
~50 s`gc.disable()` turns off automatic tracing passes; it does not turn off memory management. Reference counting still frees every object whose last reference goes away, which is most of them, so the process does not immediately balloon. It is defensible in three places: a short-lived batch process that will exit before cyclic garbage matters, a latency-critical window where you disable, do the work and re-enable, and a pre-forking server where `gc.collect()` followed by `gc.freeze()` before the fork keeps the collector from touching startup objects and dirtying pages the children share copy-on-write. What breaks is that cyclic garbage now accumulates until something calls `gc.collect()` explicitly, and any release attached to finalization is postponed with it. The setting is process-wide and invisible, so a library that creates cycles will quietly grow the footprint of code that never asked for this.
code
python · 11 linesimport gc
startup_state = {"config": {"rows_per_batch": 6800}}
gc.collect()
gc.freeze()
print("frozen objects:", gc.get_freeze_count())
print("still enabled:", gc.isenabled())
gc.unfreeze()
print("after unfreeze:", gc.get_freeze_count())go deeper
The key fact to hold is that turning the collector off does not stop Python from freeing memory — reference counting still handles everything that is not part of a cycle. Knowing gc.disable() and gc.enable() exist is enough here.
Explain precisely what stops and what continues, and that explicit gc.collect() still works afterwards. Be ready to name the accumulation risk in a long-running process and the short-lived-process case where it does not apply.
Demonstrate the measured version: collector cost quantified first, a bounded scope, an explicit collection point, and a verified memory ceiling. Knowing gc.freeze() before a fork and why it protects copy-on-write pages is the differentiating detail.
Own it as a process-wide policy affecting every dependency, not a local optimisation. Decide whether the service's lifetime and restart strategy make the accumulation acceptable, and prefer reducing cycle creation or freezing after startup over a blanket disable.
**What it actually turns off.** `gc.disable()` stops *automatic* runs of the tracing cycle collector. Reference counting is untouched and keeps freeing objects the instant their last reference disappears, which covers the overwhelming majority of allocations in normal code. Explicit `gc.collect()` still works after `gc.disable()`; only the automatic trigger is gone. `gc.isenabled()` reports the current state. This is the single most misunderstood point: disabling the collector does not disable garbage collection in Python, it disables the part that handles cycles. **Three defensible uses.** *A short-lived process.* A batch job, a CLI tool or a one-shot importer that runs for a bounded time and exits can trade memory for CPU outright. Cyclic garbage accumulates, but the process dies before that matters, and the collector's passes over a growing heap were pure overhead. *A bounded latency window.* Code with a hard tail-latency budget can disable collection around the critical section and re-enable afterwards, moving the pause somewhere it does not hurt. The discipline is that this is bounded and paired: disable, do the work, enable, and usually collect at the safe point after. *A pre-forking server.* This is the strongest case and the most Python-specific. After the parent finishes loading code and building its startup structures, `gc.collect()` compacts the picture, and `gc.freeze()` then moves every currently tracked object into a permanent generation the collector ignores from then on. Its documented purpose is exactly this: make the process copy-on-write friendly before a POSIX fork. Without it, the first collection in each forked child walks those long-lived parent objects and writes to their headers, which dirties the shared pages and forces the kernel to copy memory the workers were supposed to share. `gc.get_freeze_count()` reports how many objects are frozen, and `gc.unfreeze()` puts them back. ```python import gc gc.collect() gc.freeze() # startup objects move to the permanent generation print(gc.get_freeze_count()) ``` **What breaks.** Cyclic garbage is the obvious cost: with automatic passes gone, every cycle the code creates stays resident until someone calls `gc.collect()`. In cycle-heavy code — object graphs with back-pointers, retained exceptions holding tracebacks and frames, callback registries — that is unbounded growth on a long-running process. Finalization is delayed with it, so anything relying on a cyclic object's cleanup waits indefinitely. The setting is also process-wide and invisible. Your service imports libraries that were written expecting a collector; disabling it globally changes their memory behaviour without them or their authors knowing. Some libraries re-enable it, so the state is not even reliably yours. If you disable, own it explicitly: call `gc.collect()` at a natural quiet point — between batches, between requests, on a scheduled tick — and treat that call as load-bearing code with a comment saying why. **Cheaper alternatives to reach for first.** Raising the first collection threshold with `gc.set_threshold()` reduces pass frequency without giving up automatic collection entirely, and is reversible and measurable. Freezing after startup with `gc.freeze()` removes the long-lived graph from every future pass while leaving new allocations collected normally — often the whole win with none of the risk. And best of all, creating fewer cycles: dropping a back-pointer or holding it as a non-owning reference means the counter frees those objects immediately and the collector never has to consider them. **How to justify it in an interview.** Do not present `gc.disable()` as a performance tip. Present it as a bounded, measured decision: here is the collector cost measured with `gc.get_stats()` or a `gc.callbacks` hook, here is the process lifetime that makes the accumulation acceptable, here is where the explicit collection happens, and here is the memory ceiling we verified afterwards. A candidate who says 'we disabled the GC and it got faster' without those four things is describing a leak they have not found yet.
- What exactly does `gc.freeze()` buy a pre-forking server?It moves every currently tracked object into a permanent generation the collector never examines again. Without it, the first collection inside each forked child touches the parent's long-lived objects, which dirties the copy-on-write pages the children were sharing and multiplies resident memory by the worker count. Freezing after startup keeps those pages clean while newly allocated objects are still collected normally.
- Is raising the collection threshold a better first move than disabling entirely?Usually yes. `gc.set_threshold()` keeps automatic collection as a safety net while cutting how often it runs, so a cycle-creating dependency cannot grow the process without bound. It is also easy to bisect: try a couple of values, measure pauses with `gc.get_stats()` and memory afterwards, and keep the one that is justified by numbers.
- A team reports a large speedup from disabling the collector. What do you check?Whether the memory ceiling was measured over a realistic runtime, and where the explicit collection happens. A speedup with no ceiling check usually means cyclic garbage is accumulating and the win will be paid back as a restart. I would also ask how the collector cost was measured beforehand, because unmeasured collector time is often really allocation time.
saying these in an interview costs you the question
- Says disabling it turns off Python's memory management
- Presents gc.disable as a general performance tip
- Disables globally with no explicit collect anywhere
- Forgets the setting affects every imported library
- Confuses freezing objects with disabling collection
- Never measures the memory ceiling afterwards