Skip to main content

Python Concurrency After the GIL: Threading, Asyncio, and Free-Threading in Practice

5 min read
Python Concurrency After the GIL: Threading, Asyncio, and Free-Threading in Practice

Python 3.14 ships with an officially supported free-threaded build. For the first time in 35 years, threading.Thread can run Python bytecode on multiple cores simultaneously. The GIL is not gone. It is optional.

That single change splits Python's concurrency story into four real options: threading with the GIL, asyncio, multiprocessing, and free-threaded Python. Each solves a different problem. Picking the wrong one costs you in performance, complexity, or debugging time you did not budget for.

For most of Python's history, Python concurrency was a workaround exercise. Teams routed around the GIL with multiprocessing for parallelism, asyncio for I/O, and Python multithreading for the narrow cases where the GIL released during system calls. We have shipped Python backends under every one of those patterns, including when choosing between Python and Node.js for the job. The no-GIL era changes what the right default looks like. Here is how to think through each model.

Is the Python GIL removed in Python 3.14?

No. Python 3.14 ships an officially supported free-threaded build (python3.14t) where the GIL is disabled, alongside the standard GIL-enabled build. The GIL is now optional, not gone. For CPU-bound threaded code, the free-threaded build enables true multi-core parallelism for the first time.

What the GIL Actually Did (and Why Removing It Took 30 Years)

Understanding Python concurrency today requires knowing what the GIL was protecting and why it lasted as long as it did.

The Global Interpreter Lock is a mutex that ensures only one thread executes Python bytecode at a time. It was introduced in the earliest versions of CPython to simplify memory management: Python's reference counting system is not thread-safe, and the GIL was the cheapest way to make it safe without slowing down single-threaded programs.

For single-threaded code, the GIL is invisible. For multi-threaded code doing I/O, the GIL releases during system calls, so threads can overlap their waiting. For multi-threaded code doing CPU work, the GIL is a hard ceiling: only one thread runs at a time, regardless of how many cores are available.

Multiple attempts to remove the GIL over the years failed because they degraded single-threaded performance. That was always the wrong tradeoff: the vast majority of Python programs are single-threaded, and slowing them down to help multi-threaded programs was not viable.

What changed was PEP 703, authored by Sam Gross at Meta. It introduced biased reference counting: fast, non-atomic reference counts on the owning thread, and atomic operations only when another thread touches the same object. Add immortalisation of common objects and a new allocator (mimalloc), and the free-threaded build hits near-parity with the GIL-enabled build on single-threaded workloads.

The result: Python 3.13 shipped an experimental free-threaded build. Python 3.14 promoted it to officially supported status via PEP 779. Single-threaded overhead is now roughly 1-8% depending on platform, down from about 40% in early 3.13 builds.

The GIL is not gone. It is optional. And that distinction matters for every decision that follows.

Blog post image

Python's Four Concurrency Models: What Each One Actually Solves

Python concurrency in practice now means choosing between four models, each designed for a different problem shape. The mistake most teams make is treating them as interchangeable.

Threading (GIL-enabled)

Python's original concurrency model. Threads share memory and run within a single process, but the GIL means only one thread executes Python code at a time. Useful when your workload is I/O-bound: HTTP requests, database queries, file reads. During I/O, the GIL releases, so threads can overlap their waiting effectively. For CPU-bound work, Python multithreading with the GIL gives you zero parallelism.

Asyncio

Cooperative, single-threaded concurrency. One thread runs an event loop that switches between coroutines at await points. There is no parallelism at all. The advantage is handling thousands of concurrent I/O operations with minimal overhead. Asyncio excels at network servers, API gateways, and any workload where you are waiting on many things simultaneously. It is the foundation of frameworks like FastAPI and underpins most modern Python API development.

Multiprocessing

Spawns separate OS processes, each with its own Python interpreter and memory space. The GIL is irrelevant because each process has its own. This has been the standard answer for CPU-bound parallel processing in Python for years. Tradeoff: higher memory cost per worker, serialization overhead for passing data between processes, and more complex coordination.

Free-threaded Python

The new option. With the GIL disabled, threads run Python bytecode on multiple cores simultaneously. You get true thread-level parallelism with shared memory, lower overhead than multiprocessing, and a programming model closer to what engineers expect from other languages. Tradeoff: your code and your dependencies must be thread-safe, and the ecosystem is still catching up (more on that below).

Threading (GIL)AsyncioMultiprocessingFree-Threading
ParallelismNo (I/O overlap only)No (cooperative)Yes (process-level)Yes (thread-level)
Memory modelSharedSingle-threadedIsolated per processShared
OverheadLowVery lowHigh (per-process)Low
Best forI/O-bound tasksHigh-concurrency I/OCPU-bound workCPU-bound work
Ecosystem riskNoneAsync libraries onlyNoneCompatibility gaps

Free-Threaded Python in Practice: What Works and What Does Not

Free-threaded Python performance in practice is the question every team considering it needs to answer honestly, not from benchmarks but from real workload characteristics.

The free-threaded build is a separate binary: python3.14t (note the t suffix). You install it alongside the regular build. It is not a flag you toggle at runtime.

What works well today: CPU-bound workloads that can be divided into independent chunks and run across threads. Data transformation pipelines, batch processing, parallel computation, and ML preprocessing where you are tokenizing or feature-extracting across batches. Community benchmarks show multi-threaded speedups of 2-3.5x on 4 cores for embarrassingly parallel tasks.

What gets complicated: any code that mutates shared state. The GIL was hiding thread-safety bugs in Python programs for decades. Code that appended to a shared list, modified a shared dictionary, or updated a counter without locks was technically wrong but worked because the GIL prevented simultaneous access. In free-threaded Python, those bugs surface as race conditions, corrupted data, or crashes.

python
# This worked by accident under the GIL.
# In free-threaded Python, it is a race condition.
shared_results = []

def process(item):
    result = expensive_computation(item)
    shared_results.append(result)  # Not safe without locks

# Correct version for free-threaded Python:
from threading import Lock
results_lock = Lock()

def process_safe(item):
    result = expensive_computation(item)
    with results_lock:
        shared_results.append(result)

What does not work yet: if you import a C extension that has not declared itself free-threading safe, the interpreter silently re-enables the GIL for the entire process. Your threads keep running, but they will not run in parallel. You can check with sys._is_gil_enabled() after imports. The py-free-threading tracker, maintained by Quansight Labs, covers ecosystem readiness module by module. We go deeper on how to check this before deploying in the ecosystem readiness section below.

When to Use Asyncio vs Threading vs Free-Threading for Python Backends

Choosing the right Python concurrency model for backend work comes down to one question: where is your code spending its time?

I/O-bound backends with many concurrent connections: asyncio. An API server making database queries, calling external services, or streaming responses is I/O-bound. Asyncio handles thousands of concurrent operations on a single thread with minimal overhead. This is why FastAPI outperforms thread-per-request models for typical web API workloads despite running single-threaded. Most of our backend development work on Python APIs starts here.

CPU-bound work that isolates per request: multiprocessing or free-threading. Image processing, PDF generation, data transformation, ML inference preprocessing. Multiprocessing is the battle-tested path with no ecosystem risk. Free-threading is the newer path with lower overhead, but it requires verifying that your dependencies support it.

Mixed I/O and CPU workloads are where the call gets interesting. The traditional answer was asyncio for the I/O layer with ProcessPoolExecutor for CPU offloading. Free-threading opens a new option: asyncio for I/O with ThreadPoolExecutor for CPU work, where threads now actually parallelize. Python 3.14 even added first-class free-threading support to asyncio, enabling parallel execution of multiple event loops across threads.

Running a web framework? Check what your framework supports. ASGI servers like Uvicorn can benefit from mixed async and thread workloads. But web frameworks rarely bottleneck on CPU-bound Python code. Most of the time is spent waiting on databases and external APIs. Profile before optimizing.

The strongest signal: if you are already running multiprocessing for CPU parallelism and the memory overhead is hurting you at scale, free-threading is worth evaluating as a replacement. Same parallelism, lower memory, shared state access. If you are not hitting a concurrency bottleneck today, asyncio for I/O and multiprocessing for CPU work remains the proven default.

The Ecosystem Readiness Problem (and How to Check Before You Commit)

Python ecosystem readiness for free-threading is the practical bottleneck that decides whether you can adopt it today or need to wait.

The mechanism matters: when CPython loads a C extension that has not opted into free-threading, it re-enables the GIL for the entire process. This is not an error. There is no warning. Your program runs correctly but without parallelism. The only way to know is to check sys._is_gil_enabled() after your imports.

As of early 2026, the major libraries have made real progress. NumPy supports free-threaded builds. Pandas is working through it. PyTorch has partial support. Cython, pybind11, and PyO3 are updating their tooling. The long tail of the ecosystem, though, the smaller packages your production system likely depends on, takes longer.

Before committing to free-threaded Python for a production system:

  1. Install the free-threaded build (python3.14t) in a test environment
  2. Import your full dependency stack
  3. Confirm the GIL really is disabled. If the check returns True, something in your stack re-enabled it
  4. Use the py-free-threading tracker to identify which dependencies are not ready
  5. Run your existing test suite under the free-threaded interpreter. Existing thread-safety bugs that were hidden by the GIL will surface

Do not skip step 5. The GIL has been hiding concurrency bugs in Python codebases for decades. The free-threaded build does not create new bugs. It reveals old ones that never had a chance to manifest.

Python Concurrency Decision Table: Which Model Fits Your Workload

If you have read this far, you have enough context to use this Python concurrency decision table without oversimplifying.

Your situationRecommended model
API server, many concurrent I/O requestsAsyncio (FastAPI, Starlette)
CPU-bound batch processing, proven stack neededMultiprocessing
CPU-bound work, memory overhead of multiprocessing is a problemFree-threading (verify deps first)
ML inference preprocessing, tokenization across batchesFree-threading or multiprocessing
Mixed I/O + CPU, production stack with C extensionsAsyncio + ProcessPoolExecutor
Mixed I/O + CPU, all deps support free-threadingAsyncio + ThreadPoolExecutor (free-threaded build)
Simple I/O-bound concurrency (file reads, HTTP calls)Threading (GIL-enabled) or asyncio
Data pipeline with independent chunk processingFree-threading
Team new to Python concurrency, need safe defaultsAsyncio for I/O, multiprocessing for CPU

The clearest signal: if you are building a new Python backend today and your workload is primarily I/O-bound, asyncio is still the right default. Free-threading changes the calculus for CPU-bound work, not for I/O-bound work.

What Comes Next: Python 3.15 and Free-Threading's Road to Default

Free-threaded Python in its current form is Phase 2 of the three-phase plan outlined in PEP 703 by the Python Steering Council. Phase 1 (experimental, Python 3.13) and Phase 2 (officially supported, Python 3.14) are complete. Phase 3 would make free-threading the default build, but no specific version has been committed to.

The timeline for Phase 3 depends on ecosystem adoption. When enough libraries declare free-threading support and the single-threaded overhead is negligible, the Steering Council will evaluate making it the default. Community speculation puts this around Python 3.16 or 3.17, but that is not a commitment.

For teams making decisions today: do not wait for Phase 3 to start evaluating. The free-threaded build is stable, supported, and receiving active investment from Meta's Python runtime team and Quansight Labs. Test your stack against it now, identify which dependencies block adoption, and plan your concurrency architecture with the assumption that thread-level parallelism is coming to Python permanently.

The era of routing around the GIL is ending. The question is no longer whether Python can do real parallelism. It is whether your code is ready for it.

Procedure's engineering team has shipped Python backends on the free-threaded 3.14 build for teams hitting the memory ceiling with multiprocessing. If your team is evaluating concurrency models for a production workload, or planning to test the free-threaded build, talk to our backend engineering team about what we have seen break and what holds up. Follow our engineering work on LinkedIn.

Procedure Team

Procedure Team

Engineering Team

Expert engineers building production AI systems.

Ready to Build Production
AI Systems?

Our team has deployed AI systems serving billions of requests. Let’s talk about your engineering challenges and how we can help.

No obligation
30-minute call
Talk with engineers, not sales