free-threading

Running InvenioRDM on free-threading Python

Since the summer of 2026, TU Wien Research Data has been running on free-threading Python. This has enabled a vast simplification of its infrastructural setup (a few application containers and about 1k lines of code less), and even allowed switching on some features that were previously disabled for blocking precious Python threads/processes.

Here's an overview of the concepts involved and their relation:

Python and the GIL

For most of the history of Python, its threading model has been ruled by the Global Interpreter Lock, or GIL for short. Even though Python had APIs to support multi-threading, this lock prevented true multi-threading by restriction execution to only one thread in parallel at any time.

For the average Python application that's not a huge deal, since workarounds in the form of multi-processing (instead of multi-threading) exist. For web servers, the story is a little different...

The Web Server Gateway Interface

In 2003, PEP 333 defined the Web Server Gateway Interface (or WSGI for short) as the standard interface between Python and web servers. This interface defines a synchronous call from the server to the Python application for each HTTP request.

There exist many web frameworks that make it easy to write applications that implement the WSGI interface. One of these frameworks is Flask, which is used by InvenioRDM.

With the synchronous nature of the interface, its multi-threading capabilities are of course subject to the limitations set by the GIL.

Pre-fork web servers work around the GIL

Since simply spawning a new thread for each HTTP request is thus kind of out the window, that gave rise to the model of "pre-fork" worker model.

Web servers following this model basically first initialize the WSGI application in a master process. This initialized application then gets forked into a limited pool of as child processes ("workers") that can each handle incoming HTTP requests.

Some servers like uWSGI also support dynamically scaling the number of workers up when the pool is about to get exhausted. However, the configuration for this can be quite arcane, and each child process may require more resources.

With most web services not expecting to have long-running HTTP requests needing to go through the WSGI Python application, this is typically not a huge problem.

This can be counteracted with other mechanisms like offloading file serving to a reverse proxy via X-Sendfile HTTP headers or generally offloading file transfers to an external S3 interface. These workarounds usually come at the cost of a more complex setup though.

An asynchronous alternative to WSGI

Work on an asynchronous spiritual successor, the Asynchronous Server Gateway Interface (ASGI), started in 2016. With its non-blocking nature, this avoids the problems with long-running HTTP requests that WSGI has.

While a WsgiToAsgi adapter exists, unfortunately in some cases it's not as simple as just using it to wrap a Flask application. In a related issue on the Flask code repository, some comments mention that some mechanisms have to be adapted to work with ASGI.

In some very quick testing with an InvenioRDM application (wrapped but otherwise unmodified), I've also observed authentication not working anymore. So unfortunately, this option would require some code changes and is thus not feasible for us.

Free-threading Python

Recently, a significant milestone in the effort to remove the GIL from Python has been reached in the form of an officially supported free-threading build of Python: 3.14t. There is work underway to promote free-threading builds from a supported variant to the supported variant, starting with a stabilized ABI in the upcoming Python 3.15.

For all intents and purposes, this eliminates the problems with multi-threading in Python and brings it much closer to other programming languages in this regard. In the context of WSGI web servers that means that the old pre-fork model with child processes is no longer needed. With free-threading, it is possible to simply spawn a new thread dedicated to the handling of each incoming HTTP request.

This mode of operation is (currently only) supported by the Python web server Granian.