Runloom
Go-style stackful coroutines for Python. Write blocking code — fiber(fn),
plain recv/send, no async/await — and run a million of them across every
core in one process. Hand-rolled asm context switch + C work-stealing scheduler +
netpoll, built for free-threaded Python 3.14t (GIL off).
Runloom vs Go
Same box (64c, free-threaded CPython 3.13t), 8 hubs / GOMAXPROCS=8, warm
steady-state. Go ≈ 2.1 M spawn/s here.
The short story: on spawn, scheduling, and throughput, runloom trades blows
with Go and beats it on raw spawn — a stackful coroutine runtime on CPython
matching a compiled language even with a Python handler (596 k vs 603 k req/s at
saturation; a C handler beats Go). The one honest gap left is memory: a
suspended fiber carries a CPython eval frame, ~3.3× Go's per-fiber RSS.
Full cross-runtime numbers + cold spawn-vs-N curves: benchmark report
· perf summary.
Install
Prebuilt wheels (no compiler needed) for CPython 3.11–3.14 on Linux
(x86_64/aarch64), macOS (arm64/x86_64), Windows (AMD64); source build elsewhere.
No runtime dependencies.
What it is
Hand-rolled asm context switch (x86_64 SysV, aarch64) — ~80 ns/swap, no
syscall; Windows Fibers / POSIX ucontext fallback.
M:N work-stealing scheduler (3.13t) — Chase-Lev deque per hub, per-hub MPSC
submission, woken goroutines routed back to their origin hub.
Per-goroutine PyThreadState snapshot — cframe, datastack, exc_info,
contextvars, recursion; a million yielded goroutines share their hub threads
with no frame-chain cliff.
netpoll — epoll / kqueue / IOCP / WSAPoll / select; goroutines park
transparently on fd readiness, lost-wake-free 3-state park-commit.
Go-style channels — Chan(capacity), select, for v in ch.
Stall isolation + recovery — one unanticipated blocking call stalls only
its hub, and the runtime detects + recovers it (default on, 3.13t).
monkey.patch() makes blocking stdlib (socket, time, threading, …)
cooperative, so existing blocking code runs unchanged.
Already have async def code? The runloom.aio bridge runs it on the
single-threaded scheduler (runloom.aio.run(main()) ≈ asyncio.run) — a
zero-rewrite port path, not a multi-core speedup (use the sync API with
run(n>1, main) for that).
Honest limitations
The multi-core win needs free-threaded CPython 3.13t (3.11+ for the frame
snapshot at all). On a GIL build runloom still runs — cheap spawn, the
goroutine model, netpoll — but single-core like asyncio.
runloom doesn't make Python faster per core. CPython's ~80 k pure-Python
ops/s/core is a constant it can't raise; it lets one process hit that on every
core at once with a blocking model. The scheduler itself is Go-class.
Higher memory per goroutine than Go (~3.3× for an empty fiber — the CPython
eval frame; a C handler closes most of it).
Preemption fires only at Python bytecode boundaries — a goroutine inside a
tight pure-C call (e.g. numpy) holds its hub until it returns (same as Go +
cgo).
Linux x86_64 / 3.13t is the primary, heavily-validated target (2 M-conn
runs, fuzzing, sanitizers, formal models); other backends are maintained
in-step but less deeply exercised.
Platform support
Docs & layout
Full guide in docs/:
Quickstart ·
Asyncio bridge ·
Sync API ·
Channels ·
M:N parallelism ·
Cookbook ·
API reference
Build from source (contributors): pip install -e . from a clone (needs a C
compiler; scripts/install.sh / scripts\install.bat bootstrap one). To hack on
runloom against free-threaded CPython, use a 3.13t interpreter.