Skip to content
← Back to explainers

Python explainer

Check the Exact Python Version When You Debug the Collector

About 4 min to read. Allow extra time to work through examples.

Python 3.14.0 to 3.14.4 shipped an incremental garbage collector, and 3.14.5 went back to the old one. Log the exact version and build your service runs, and recheck collector tuning after every upgrade.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your service had a latency spike right after a deploy. A teammate rules out the garbage collector: "We run Python 3.14, and 3.14 collects in small steps, so it cannot pause for long." Whether that is true depends on which 3.14 ran.

The 3.14 collector changed, then changed back

Python 3.14.0 to 3.14.4 shipped an incremental garbage collector. It split the work on long-lived objects into small steps, to make each pause shorter. After reports of much higher memory use in production, Python 3.14.5 went back to the generational collector from 3.13. That one sorts objects into three generations by how many collections they have survived, and checks the youngest most often. Python 3.15 uses the generational collector too.

Three gc calls behave differently in the two designs:

Call 3.14.0 to 3.14.4 3.12, 3.13, 3.14.5 and later
gc.collect(1) Collects the young generation and one step of the old one Collects the two youngest generations
gc.get_objects(1) Returns nothing, because there is no generation 1 Returns the objects in generation 1
gc.set_threshold(t0, t1, t2) Ignores t2 Uses t2

Even on 3.14.0 to 3.14.4, gc.collect() with no argument ran a full collection. So a full pause was still possible on those versions.

Check your understanding A teammate says, "We run Python 3.14, so a long collector pause cannot explain the spike." What do you check? Show answer

First find the exact version and build of the process that was running at the time. Then look for explicit gc.collect() calls in your code and its libraries, because even the incremental collector ran full collections when asked. Finally, check whether collections happened during the spike.

Record what actually ran

requires-python = ">=3.14" in pyproject.toml says which versions the project supports, not which one ran. A Docker tag such as python:3.14-slim can point at a newer image next week. Have the running process report itself at startup:

import gc
import json
import platform
import sys
import sysconfig

runtime: dict[str, object] = {
    "implementation": platform.python_implementation(),
    "version": sys.version,
    "platform": platform.platform(),
    "machine": platform.machine(),
    "free_threaded_build": bool(sysconfig.get_config_var("Py_GIL_DISABLED")),
    "gc_enabled": gc.isenabled(),
    "gc_thresholds": list(gc.get_threshold()),
}
gil_probe = getattr(sys, "_is_gil_enabled", None)
runtime["gil_enabled"] = bool(gil_probe()) if callable(gil_probe) else None
print(json.dumps(runtime, sort_keys=True))

The script only reads settings, and it runs on Python 3.12 and later. free_threaded_build says whether this is a free-threaded build, one without the GIL. gil_enabled says whether the GIL is on right now, because a free-threaded build can turn it back on. Python 3.12 has no way to report that, so the value is null there.

Write this line to your startup log. Also record the application's revision, its dependency versions and the image digest, which the script cannot see. The thresholds it logs are the current settings, which your code or a library may already have changed.

Check your tuning after every upgrade

If your code calls gc.set_threshold, read the gc documentation for the exact version you deploy. Doubling t2 did nothing on 3.14.0 to 3.14.4, and it matters again from 3.14.5. Then compare the old and new versions under the same load. Count the collections and time them, and watch memory and request latency.

Pin the image, and keep updating it

A tag can move, but a digest such as python@sha256:... names exactly one image. Record the digest you deployed. A newer patch release can fix bugs you care about, so update the pin on a schedule and rerun your checks. Keep the previous digest, so that you can roll back.

Going further

step 33 of 38

Memory, Copies and Slow Code

Up next Keep Forked Workers Sharing Memory with gc.freeze