WSGIPerInterpreterGIL
- Description:
Enable per-interpreter GIL for Python sub interpreters.
- Syntax:
WSGIPerInterpreterGIL On|Off- Default:
WSGIPerInterpreterGIL Off- Context:
server config
Controls whether Python sub interpreters created by mod_wsgi are given
their own GIL, as introduced by PEP 684 in Python 3.12. When set to
On, each sub interpreter runs with its own independent GIL rather
than sharing the process-wide GIL with every other interpreter.
For background on the three GIL modes mod_wsgi supports and worked examples mixing them across processes, see GIL Modes and Free-Threading.
To enable per-interpreter GIL process-wide, set the directive at server config scope:
WSGIPerInterpreterGIL On
To enable it only for selected interpreters, place it inside a
WSGIInterpreterOptions container with process-group= and/or
application-group= selectors. A nested setting overrides the
top-level default for the matching interpreters only.
The main Python interpreter always uses the original process-wide GIL
and cannot be given its own. A WSGIPerInterpreterGIL On setting
that resolves to the main interpreter (for example a container with
application-group=%{GLOBAL}) is silently ignored for that
interpreter.
When a sub interpreter is created with its own GIL, mod_wsgi notes that fact in the Apache error log entry that records the interpreter’s creation.
Python version requirements
Per-interpreter GIL requires Python 3.12 or later. On older Python versions the directive is accepted but logs a configuration-time warning and has no effect; sub interpreters continue to share the process-wide GIL.
A free-threaded Python build (PEP 703) does not by itself prevent the
directive from working. mod_wsgi defaults to running with the GIL
enabled even on free-threaded builds, so per-interpreter GIL applies
normally unless free-threading has been opted in for the process via
WSGIFreeThreading. When free-threading is active in the
process, WSGIPerInterpreterGIL is a per-interpreter no-op (there
is no GIL to allocate per interpreter); a warning is logged at sub
interpreter creation time.
C extension compatibility
A Python C extension imported by an interpreter that has its own GIL
must declare its support for multiple per-interpreter GILs. Modules
using PEP 489 multi-phase initialisation declare this with a
Py_mod_multiple_interpreters slot whose value is
Py_MOD_PER_INTERPRETER_GIL_SUPPORTED. Modules that do not declare
support, or that explicitly declare Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED,
will fail to import in an own-GIL sub interpreter.
This is the most common first-time failure mode when enabling the directive: extensions that work fine under the shared GIL refuse to import once their interpreter has its own. Pure Python modules and extensions that have been audited for sub-interpreter safety are unaffected.
When per-interpreter GIL is enabled for an interpreter, that interpreter’s switch interval can be set independently of other interpreters in the same process via WSGISwitchInterval placed inside a matching WSGIInterpreterOptions container. Under the shared GIL the switch interval is process-global and cannot meaningfully differ between interpreters.
Experimenting with sub-interpreter load balancing
Per-interpreter GIL is a recent CPython feature and how to make best use of it in a WSGI hosting context is still being explored. The recipe below sketches one way to experiment: route incoming requests across a fixed set of named sub-interpreters in a single daemon process, each holding its own GIL, so CPU-bound work can proceed in parallel within one process.
Three pieces compose:
WSGIPerInterpreterGIL Onenables the per-interpreter GIL for sub interpreters mod_wsgi creates.A WSGIDispatchScript picks an application group name per request (the sub-interpreter to route into).
WSGIApplicationGroup %{ENV:APPLICATION_GROUP}(or theapplication-group=option onWSGIScriptAlias) tells mod_wsgi to read the application group from the environment variable the dispatch script populates.
A simple round-robin dispatch script that spreads requests across four named sub-interpreters:
# /etc/apache2/wsgi/dispatch.py
import itertools
import threading
_lock = threading.Lock()
_counter = itertools.count()
INTERPRETERS = ['interp_0', 'interp_1', 'interp_2', 'interp_3']
def application_group(environ):
with _lock:
n = next(_counter)
return INTERPRETERS[n % len(INTERPRETERS)]
The matching Apache configuration runs the application in a single daemon process with enough threads to drive all sub-interpreters concurrently:
WSGIPerInterpreterGIL On
WSGIDispatchScript /etc/apache2/wsgi/dispatch.py
WSGIDaemonProcess myapp processes=1 threads=15
WSGIScriptAlias / /srv/myapp/wsgi.py \
process-group=myapp \
application-group=%{ENV:APPLICATION_GROUP}
Under mod_wsgi-express the equivalent combines
--application-group for the env-var expansion with
--include-file for the directives express does not
have first-class options for. With per-interp-gil.conf
containing:
WSGIPerInterpreterGIL On
WSGIDispatchScript /tmp/dispatch.py
the express invocation is:
mod_wsgi-express start-server wsgi.py \
--processes 1 --threads 15 \
--application-group %{ENV:APPLICATION_GROUP} \
--include-file /tmp/per-interp-gil.conf
Caveats:
processes=1is deliberate. Multiple daemon processes would each have their own copies of the sub-interpreter set, so the dispatch script would distribute requests across distinct address spaces rather than across parallel sub-interpreters in one process. Whether a multi-process shape is the right one is a separate experiment.Each Apache child runs its own copy of the dispatch script and so has its own round-robin counter; the global distribution across all Apache children is approximately round-robin but not strictly. A hash-based dispatch (
hash(client_ip) % N) is globally deterministic at the cost of uneven distribution under skewed traffic.Every Python C extension imported into a sub interpreter must declare PEP 489 multiple-interpreter support, as covered in C extension compatibility. Failure modes when an extension does not support it surface at import time, before traffic flows.