Four ways to say "what runs per batch". Same fan-out, same batching, same recovery — only the per-batch executor changes.
Your payload is a Python function. Use it for custom logic: parsing, validation, feature extraction, filtering.
import json
def clean(batch):
out = []
for line in bytes(batch.data).split(b"\n"):
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except ValueError:
continue # malformed rides the retry path, then poisons
if rec.get("duration_ms", 0) >= 100:
out.append(json.dumps(rec).encode())
return b"\n".join(out) if out else None
forkrun.map(clean, "events.jsonl", workers=8, order="index")- Payload forms: a callable (fork-inherited, never pickled)
or
"pkg.mod:func"(imported post-fork in the worker, keeping the parent free of native imports). - Throughput (28 workers, 5M records): ~1.6M/s light,
~700k/s medium — at parity with
ProcessPoolExecutor.
Your payload is an external command. Use it for existing CLI
tools — gzip, sed, tr, grep, your own binaries.
forkrun.map("gzip -c", "logs.txt", mode="spawn")
forkrun.map(["sed", "s/old/new/"], "data.txt", mode="spawn")stris split on whitespace; use thelistform whenever quoting would matter.- Batch bytes go to the command's stdin; stdout is captured as the result. Non-zero exit rides retry-then-poison like any payload error (missing commands exit 127, same path).
c_spawn_loop=True(opt-in,map()only, UMA only) runs the claim→spawn→ack loop in C. Measured at parity with the Python loop (±4%) — its value is architectural uniformity (one engine-owned recovery path), not speed.
Your payload is a C function in a .so. Use it for maximum
throughput when you can write C.
forkrun.map("./myplugin.so:process", "data.txt", mode="plugin")- Two conventions, not one: the default v0 Python path calls
a one-arg entry point,
int process(struct fr_py_plugin_ctx *ctx)(72B Python-side buffers, return code captured — NOT the frozen engine ABI). Only dialect-tagged plugins (forkrun_use_ctx1/2) dispatch through the C-level v1 path on the frozen ABI (struct forkrun_ctx, 128B: the plugin reads the input window zero-copy and writes stdout, which the shim stages and frames) — and only those work from both bash-Cand Python unchanged. An untagged legacy two-arg bash plugin called through v0 would receive the ctx pointer asargcand garbage asargv; the parent probes the tag and never guesses. See PLUGINS.md. - Throughput (28 workers, 5M): 5.5M/s light, 2.3M/s medium (yyjson single-pass), 710k/s heavy — 2–7× the Python loop.
c_worker_loop=True(opt-in,map()only, dialect-1/2 plugins, UMA only) moves the whole worker loop into C.- Writing one: PLUGINS.md.
Kernel passthrough — no computation, just data movement. Use it to split/parallelize I/O at memory bandwidth.
forkrun.map(None, "input.bin", mode="splice", bytes=512*1024)
for chunk in forkrun.stream(None, "input.bin", mode="splice"):
process(chunk) # raw byte-windows as they arrive- Payload must be
None; byte batching only (bytes=N, default 512KB —lines=is rejected, boundaries are never detected);run()is rejected (passthrough produces output — usemap()/stream()). - Workers run the claim→sendfile→ack loop with zero Python per batch.
| Situation | Mode |
|---|---|
| Custom Python logic | python |
| Existing CLI tool or binary | spawn |
| Hot loop worth writing in C | plugin |
| Just moving bytes | splice |
| Per-batch Python overhead matters | plugin + c_worker_loop, or spawn + c_spawn_loop |
Throughput expectations (honest, same-boot measured): PERFORMANCE.md.