Python’s GIL and the Concurrency Model It Encodes
The Global Interpreter Lock was never an arbitrary restriction someone bolted onto Python out of laziness. It was the cheap, workable answer to a real problem that reference counting creates under threads. Free-threaded Python does not throw that problem away, it finally pays the cost that answer had been deferring for thirty years.
Ask most Python developers why the GIL exists and the answer usually stops at “it makes the interpreter simpler.” That is true, but it skips the actual mechanism, and the mechanism is what makes free-threaded Python’s arrival in Python 3.14 a genuinely interesting engineering story rather than just a switch someone finally flipped.
The Original Bargain: Reference Counting Needs Somebody to Watch It
CPython manages memory primarily through reference counting. Every object carries a count of how many references point to it, that count goes up when a reference is copied and down when one goes out of scope, and the object is freed the instant the count hits zero. This is why closing a file or releasing a lock in Python tends to happen predictably, right when the last reference disappears, rather than at some unpredictable later moment.
That predictability has a cost under threads. Incrementing or decrementing a reference count is a read-modify-write operation, and if two threads perform one on the same object at the same time without coordination, the update can be lost. An object can end up with the wrong count, which leads to either a premature free while something still holds a reference, or a leak that never gets cleaned up. Multiply that by every single object handoff happening anywhere in a running program, and you have a synchronization requirement that touches essentially every line of Python code, not just the parts a developer thinks of as “shared state.”
PEP 703, the proposal that eventually removed this constraint, describes the underlying tension plainly: the GIL prevents multiple threads from executing Python code at the same time, and it exists because CPython’s reference counting was never made safe for concurrent access. Rather than putting a lock around every individual object’s reference count, which would have meant enormous per-operation overhead and a nightmare for C extension authors to reason about, the original design used one lock around the entire interpreter. Whichever thread holds the GIL is the only thread allowed to touch Python objects at all, so reference counts stay consistent by construction. It was a single global answer to a problem that would otherwise require thousands of small, precise answers.
The Road Tracing Collectors Took Instead
Runtimes built around tracing garbage collection, the JVM and V8 among them, sidestep this specific problem because they never rely on a live count attached to each object during normal execution. A tracing collector periodically walks the graph of objects reachable from a set of roots and reclaims anything it cannot reach, rather than reacting to every individual reference change as it happens. Ordinary mutator threads can keep running largely independently between collection cycles; the synchronization cost is concentrated into the collection pause or the concurrent marking phase, not spread across every assignment statement in the program.
That is not a free lunch, it is a different bill. Modern collectors like G1 and ZGC on the JVM still need write barriers, safepoints, and careful coordination to collect concurrently with running threads, and stop-the-world pauses have not disappeared, they have just gotten shorter and more predictable. What tracing collectors trade away is the thing Python’s refcounting model was prized for: deterministic, immediate destruction. An object’s finalizer or a Java `close()` call under a tracing collector runs whenever the collector eventually gets to it, not the instant the last reference disappears, which is part of why Java leans on `try`-with-resources rather than trusting finalization for anything time-sensitive. Python’s ecosystem has spent decades depending on the opposite guarantee, and that is a real reason CPython never simply swapped reference counting for a tracing collector to solve the threading problem outright.
What Free-Threaded Python Actually Changes
PEP 703, authored by Sam Gross at Meta and accepted by the Steering Council in October 2023, does not abandon reference counting. It makes reference counting itself safe for concurrent access, largely through biased reference counting, which gives each object an owning thread and lets that thread update the count through a fast, non-atomic path while other threads fall back to a slower atomic path, plus deferred reference counting and immortal objects for values like small integers and interned strings that never need counting at all. Containers gain per-object locks rather than relying on one global one. The effect is closer to distributing the tracing collector’s write-barrier idea across a refcounting scheme than to replacing refcounting with something else entirely.
The rollout has been deliberately staged. Phase one shipped an explicitly experimental free-threaded build in Python 3.13, with roughly 40 percent single-thread overhead. Phase two, governed by PEP 779, made the build officially supported, though still optional, in Python 3.14, released in October 2025.

On the workloads free-threading was built for, CPU-bound code spread across multiple threads, independent benchmarking has measured speedups of roughly 3.5x on four cores, and Meta’s own internal tests reported near-linear scaling with core count on suitable workloads. That is the upside half of the bargain: the interpreter no longer serializes bytecode execution across threads, so CPU-bound Python code can finally use more than one core in a single process without reaching for multiprocessing.
The Bill the Ecosystem Still Has to Pay
The GIL was never only a memory-safety mechanism for CPython’s own internals. It quietly gave every C extension author an implicit atomicity guarantee: as long as your extension only touched Python objects while holding the GIL, you never had to think about thread safety yourself. Removing the GIL removes that free guarantee too, which is why a free-threaded interpreter automatically re-enables the GIL for the whole process the moment it imports a C extension that has not explicitly declared itself thread-safe.

That halfway mark is a meaningful milestone, but it also means the other half of the most-used compiled packages in the ecosystem will silently put the GIL back the moment a project imports them. Free-threading is real and it works, but whether any given application actually benefits from it today depends less on CPython itself and more on whether its dependency tree has caught up.
| Model | How memory is reclaimed | What threads need to coordinate on | What it costs |
|---|---|---|---|
| CPython reference counting (pre-3.13) | Immediately, when a count hits zero | Every reference count update, everywhere | One process-wide lock (the GIL) serializing all bytecode execution |
| CPython free-threaded (3.13+) | Immediately, via biased/deferred refcounting | Only cross-thread reference updates, via per-object fast/slow paths | 5–10% single-thread overhead, 15–20% more memory, ecosystem migration |
| Tracing GC (JVM, V8) | Periodically, via reachability from roots | Collection pauses and write barriers, not individual assignments | Non-deterministic finalization; concurrent collector complexity |
Deciding whether free-threaded Python is worth adopting today
- Check your dependency tree against the py-free-threading tracker before committing to anything; a single unready C extension silences the whole benefit for that process.
- Free-threading targets CPU-bound, thread-parallel workloads specifically. I/O-bound services that are already fine with asyncio or GIL-enabled threading have little to gain from switching.
- If one dependency lags behind, `python3.14t -X gil=1` lets you keep the free-threaded interpreter while re-enabling the GIL selectively, rather than abandoning the build entirely.
- Benchmark on your own workload before deploying. The 5 to 10 percent single-thread tax is real, and it only pays for itself once genuine multithreaded parallelism is in the picture.
What We Learned
The GIL was the cheap side of a real tradeoff: CPython’s reference counting model gives Python immediate, deterministic object cleanup, but that immediacy means every reference count update needs protecting from concurrent access, and a single global lock was a far simpler answer to that than locking every object individually. Tracing collectors like the JVM’s avoid the problem by reclaiming memory periodically rather than reactively, at the cost of losing that same immediacy. PEP 703’s free-threaded build does not choose the tracing side of that tradeoff; it keeps reference counting and makes it safe for concurrent access instead, through biased and deferred reference counting, at a measured cost of roughly 5 to 10 percent single-thread overhead and 15 to 20 percent more memory in Python 3.14.
The remaining open question is not technical so much as logistical: about half of the most-used compiled packages on PyPI have shipped free-threading-ready wheels as of early 2026, and any dependency that has not will quietly put the GIL back the moment your program imports it.


