# Garbage collection pauses in tests (gc.freeze)

> Python's cyclic garbage collector rescans every long-lived object in a big test suite. Call gc.freeze() after collection and raise gc thresholds to cut GC pauses in pytest.

CPython frees most objects as soon as their reference count drops to zero. Objects that refer to each other in a cycle never get there, so a separate cyclic garbage collector runs from time to time, walks the objects that can form cycles, and frees the unreachable ones.

A test suite is a bad case for it. pytest has imported your whole application, every test module and every plugin before the first test runs: millions of functions, classes, dicts and model instances that live until the end of the session. Tests then allocate quickly (ORM objects, mocks, factories, request and response objects) and the collector keeps running, and its full collections walk all of those long-lived objects again, every time.

## Why it's slow

The collector sorts objects into generations. New objects start in the youngest one; objects that survive a collection move to an older one. A collection starts when allocations minus deallocations since the last one exceed the first threshold, and older generations are collected less often. A full collection examines every tracked object, including everything imported at startup, and that's the expensive part: its cost grows with the size of the heap, not with the amount of garbage.

## The fix: freeze what's already there

`gc.freeze()` moves every object the collector currently tracks into a permanent generation that later collections ignore. Call it once pytest has imported everything, and the collector only looks at objects created after that point:

```python
# conftest.py
import gc


def pytest_collection_finish(session):
    gc.collect()
    gc.freeze()
```

`pytest_collection_finish` runs after all test modules are imported, so their objects are frozen too. Calling `gc.collect()` first frees the garbage left over from startup, so it isn't frozen along with the live objects. If you prefer `pytest_sessionstart`, it works too, but at that point the test modules haven't been imported yet. Under pytest-xdist each worker collects on its own and runs the hook itself.

## Collect less often: gc.set_threshold

The first threshold decides how many net allocations trigger a young collection. Raising it means fewer, slightly larger collections:

```python
# conftest.py
import gc


def pytest_collection_finish(session):
    gc.collect()
    gc.freeze()
    gc.set_threshold(50_000, 50, 100)
```

Check your defaults with `gc.get_threshold()`: it's `(700, 10, 10)` on Python 3.12 and `(2000, 10, 10)` on recent 3.13 and 3.14 releases. Version notes:

- Python 3.14.0 to 3.14.4 shipped an incremental collector with only two generations. On those versions the second threshold sets how fast the old generation is scanned and the third is ignored.
- Python 3.14.5 reverted to the generational collector from 3.13, so thresholds mean the same as on 3.13 again.

Avoid `gc.disable()` for the whole session. It stops cyclic garbage from ever being freed, and on a large suite memory keeps growing until the run is slow for other reasons or the CI machine runs out.

## Measure before and after

`gc.callbacks` calls your function at the start and end of every collection, which is enough to time them:

```python
# conftest.py
import gc
import time

gc_seconds = 0.0
gc_started = 0.0


def track_gc(phase, info):
    global gc_seconds, gc_started
    if phase == "start":
        gc_started = time.perf_counter()
    else:
        gc_seconds += time.perf_counter() - gc_started


gc.callbacks.append(track_gc)


def pytest_terminal_summary(terminalreporter):
    terminalreporter.write_line(f"garbage collection: {gc_seconds:.2f}s")
```

Under pytest-xdist the summary runs in the controller process, so this only reports runs without `-n`. On Python 3.15, `gc.get_stats()` also has a `duration` key per generation.

## Keep the tests correct

- This trades memory for speed. Frozen objects are still freed when their reference count drops to zero, but a frozen object that ends up in a reference cycle is never collected, and a higher threshold lets more garbage pile up between collections. Watch memory on large suites, especially in CI with several xdist workers on a small machine.
- Tests that call `gc.collect()` to check that something is freed (weakref or finalizer tests) still work for objects they create: anything created after the freeze is collected as usual.
- Keep it in `conftest.py`. Don't copy test GC settings into production code without measuring there.

## How flakytest finds it

flakytest puts samples taken while the garbage collector runs in the *gc* category. When garbage collection is at least a second and 3% of test time, it suggests **The garbage collector is a noticeable share of test time**, with the tests that paid the most. flakytest also measures each test's memory, so you can check what freezing and higher thresholds did to it in the same report.

## Further reading

- [Python: `gc.freeze()`](https://docs.python.org/3/library/gc.html#gc.freeze)
- [Python: `gc.set_threshold()`](https://docs.python.org/3/library/gc.html#gc.set_threshold)
- [Python: `gc.callbacks`](https://docs.python.org/3/library/gc.html#gc.callbacks)
- [What's new in Python 3.14: garbage collection](https://docs.python.org/3/whatsnew/3.14.html)
