Skip to content

Runtimes

One graph, several ways to run it. The graph never changes; only the runtime argument does.

Runtime Nodes run as Pick it when
threads (default) one thread per node I/O-bound stages, numpy and C extensions, and full parallelism on free-threaded CPython 3.14t
processes farm workers in spawned child processes, the rest as threads here CPU-bound pure Python on a GIL build
async nodes coroutines on an event loop, a farm of them as one pool hundreds of concurrent network requests
distributed each group as threads or processes on its host, TCP between groups two or more machines
sync one node at a time in a fixed order, in the calling thread tests, debugging, deterministic runs; deadlocks reported at once

The numbers behind the table

Measured on a 16-thread machine, a farm of eight CPU-bound pure-Python workers, 2.4 s of sequential work:

speedup
3.13 threads (GIL) 0.9x
3.13 processes 5.5x
3.14t threads 5.6x

Processes on a GIL build match the free-threaded interpreter. The remaining gap to 8x is process startup, pickling each item, and the parent's own threads.

Threads

Every node is a thread waiting on one inbox. Channels are bounded (default 1024 items) and carry batches (default 32 items per hand-off, flushed after one millisecond or when the producer would block), which removes most lock traffic. On the free-threaded interpreter this runtime scales like the Java original; on a GIL build it parallelizes only work that releases the GIL.

Processes

runtime="processes" puts every farm worker in its own spawned child; tq.farm(..., runtime="processes") does it for one farm. In the parent the worker keeps its inbox, outbox and edges as a proxy; in the child it runs unchanged, so tags, ordered and gather collection, on-demand scheduling and feedback loops all work. A completion marker per item keeps credits exact across the pipe. A crashed child is WorkerDied; an exception is the same NodeError. Send rows, chunks or arrays rather than scalars.

Async nodes

An async def node runs on an event loop; tq.farm(coro, workers=N) runs N coroutines at a time on one thread. Coroutines take the item only (no ctx) and return or yield what to send; ordered=True keeps input order; async classes may have async on_start and on_end. Other emitter and collector policies do not apply to a pool.

Distributed

A deploy file cuts the graph into named groups with an endpoint each. Every host runs the same flow with its group name; tolquane launch deploy.toml flow.py starts them all from one terminal, over ssh where needed. Edges between groups become TCP channels: numbered batches, acknowledgements that return the producer's credits, resend after a dropped connection, an optional shared secret. A failing group tells its peers (PeerFailed); an unreachable one is PeerLost. Loops and an ordered farm's ends stay in one group.

Sync

One node runs at a time and hands over when it blocks, in graph order. The same graph gives the same interleaving every run, a deadlock is detected the instant it happens, and a debugger steps through it. Process placement is ignored, so a process flow can be debugged here first.