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.