SEDA bus · Python · 0.2.0
Does the SEDA model finally pay off without the GIL?
seda-bus-python exists specifically to test free-threaded CPython (PEP 703). Under the
GIL, sharing a thread pool across CPU-bound stages is close to pointless — threads don't run
bytecode in parallel. This port is benchmarked in both a free-threaded build and a standard GIL
build, so the difference is measured, not assumed.
concurrent.futures.ThreadPoolExecutorra-common onlyra_common.Envelope (rewired at 0.2.0)DynamicRoutingSlip — LIFOthreading.BoundedSemaphore per channel__enter__/__exit__) or explicit start()How it implements the design
The Enum-and-Protocol version of the shared design.
The queue is a collections.deque guarded by a threading.Condition;
consumers are a Consumer Protocol (runtime-checkable) or any callable,
wrapped internally. Delivery and Backpressure are Enums;
ergonomics lean into keyword arguments —
bus.channel(name, capacity=..., concurrency=..., delivery=..., backpressure=...,
max_attempts=...).
As of the 0.2.0 rewire, it carries ra_common.Envelope rather than its own struct;
seda_bus.envelope re-exports it and adds make_envelope/
target_service ergonomic helpers over the richer type's LIFO routing slip. Per-hop
attempts moved off the envelope onto the channel, keyed by id, since
ra_common.Envelope carries none.
The port's own README documents the real payoff this benchmark exists to test: on genuinely
CPU-bound work (a hashcash/fib workload, not this benchmark's near-zero-cost consumer), 24
envelopes through one stage scale roughly 2.6× across a 12-core pool on free-threaded
CPython (python3.14t, PYTHON_GIL=0), after a roughly 1.3×
single-thread tax — while under the GIL, adding workers to the same workload makes it
slower.
Results
Free-threading wins on this benchmark too — just not for the reason you'd guess.
| Build | Config | Sustained eps | 0.5× | 1.0× | 1.5× |
|---|---|---|---|---|---|
| 3.14t (free-threaded) | cap1 | 100,868 | 0.92 | 0.94 | 0.67 |
cap8 | 25,366 | 0.95 | 0.95 | 0.79 | |
| 3.13 (GIL) | cap1 | 41,126 | 0.95 | 0.95 | 0.68 |
cap8 | 16,648 | 0.94 | 0.94 | 0.89 |
| Build | Config | eps | vs. seq |
|---|---|---|---|
| 3.14t (free-threaded) | seq | 104,602 | — |
par | 45,263 | 0.43× | |
chan | 270,904 | 2.59× | |
| 3.13 (GIL) | seq | 51,649 | — |
par | 28,417 | 0.55× | |
chan | 35,285 | 0.68× (collapse) |
Reading the numbers
This benchmark's consumer barely uses the CPU — which is itself the finding.
This benchmark's consumer does almost no CPU work, so these numbers are mostly about
lock/GIL contention under a shared channel, not the CPU-bound benefit the port's own README
measures. Under this corrected, construction-excluded methodology, free-threaded's
par is a real 0.43× collapse rather than the roughly flat number an earlier,
less careful pass measured — removing the shared lock (chan) still shows the
real signal, a 2.59× gain with no contention point left to hide behind.
The GIL build's collapse is real in both configurations (par
0.55×, chan 0.68×) — the GIL serializes bytecode execution
regardless of channel topology. Both Python variants' cap8 holds up better than
cap1 at 1.5× overload, but for an unglamorous reason: cap8's
absolute sustained ceiling is 4–6× lower than cap1's to begin with, so
150% of a small number is easier to approach than 150% of a large one.
The full cross-language results, including the GIL-vs-free-threaded discussion
Source
Read the code, or the rest of the family.
seda-bus-python on GitHub
— source, pytest suite, and the raw-interpreter microbenchmarks in bench/.