Real Goroutines for Python 3.13t+ free-threaded.
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).
import threading, runloom from urllib.request import urlopen runloom.monkey.patch() def crawl(url): # urlopen() looks blocking -- but monkey.patch() parks the goroutine on the # socket instead of the OS thread, so all 64 fetches overlap on real cores. body = urlopen(url, timeout=10).read() print(threading.get_native_id(), len(body)) def main(): for _ in range(64): runloom.fiber(crawl, "http://example.com") runloom.run(8, main) # 8 hub threads -> real cores on 3.14t (GIL off)
Same box (64c, free-threaded CPython 3.13t), 8 hubs / GOMAXPROCS=8, warm
steady-state. Go ≈ 2.1 M spawn/s here.
| metric | runloom | Go | verdict |
|---|---|---|---|
spawn — pure C (c_entry) |
2.29 M/s | 2.10 M/s | beats Go |
spawn — Python (runloom.fiber) |
1.35 M/s | 2.10 M/s | 0.65× |
| context switch | ~75 ns yield · ~560 ns chan RT | ~50 ns Gosched |
~parity |
| conn/s — churn (new conn per req) | ~75–78 k/s | ~75–78 k/s | parity |
| req/s — keep-alive echo, Python handler | 596 k/s | 603 k/s | 0.99× — parity (C handler beats Go) |
| memory — empty parked fiber | 8.8 KB | 2.7 KB | 3.3× (the one real gap) |
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.
runloom.optimize("throughput") # runloom.fiber -> max spawn rate (fiber_fast)
runloom.optimize("memory") # runloom.fiber -> small right-sized stacks (default)
pip install runloom
import runloom # scheduler + channels, plus monkey/time/context/sync/aio
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.
ucontext fallback.PyThreadState snapshot — cframe, datastack, exc_info,
contextvars, recursion; a million yielded goroutines share their hub threads
with no frame-chain cliff.Chan(capacity), select, for v in ch.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).
numpy) holds its hub until it returns (same as Go +
cgo).| OS / arch | switch | netpoll | tested |
|---|---|---|---|
| Linux x86_64 | fcontext-asm | epoll | yes — hw, 3.11 / 3.12 / 3.13t / 3.14t (primary) |
| Linux aarch64 | fcontext-asm | epoll | qemu |
| macOS x86_64 / arm64 | fcontext-asm | kqueue | hw, 3.14t |
| FreeBSD / GhostBSD | fcontext-asm | kqueue | hw, 3.12 |
| Windows 10/11 / Server 2022 | Fibers | IOCP→WSAPoll→select | hw, 3.14t |
| Solaris / Android / other BSD | ucontext / asm | select / epoll / kqueue | review |
Full guide in docs/: Quickstart · Asyncio bridge · Sync API · Channels · M:N parallelism · Cookbook · API reference
| Dir | Contents |
|---|---|
src/runloom_c/ |
C extension: scheduler, channels, netpoll, asm backends, M:N hubs, stall recovery |
src/runloom/ |
Python layers: aio, sync, monkey, time, runtime |
tests/ · examples/ · benchmark/ · docs/ |
tests · runnable examples · benchmarks + perf harness · docs |
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.