Worker runtime
Most runtimes give you one worker per core and let you guess which task is blocking. Yatta splits work into two pools so the answer is built in.
Two pools
The scheduler reads navigator.hardwareConcurrency at startup and sizes two pools from it.
- cpu-pool —
cores * 0.35workers, concurrency 1. Hashing, crypto, compression. One task at a time so a heavy job cannot starve its neighbours. - io-pool —
min(16, cores * 0.8)workers, concurrency 500. Database queries, HTTP, mail. Many tasks in flight per worker.
On a two-core machine that is 1 CPU worker and 2 I/O workers. On small hosts I/O workers also start with Bun's smol heap to keep memory down.
Subsystems
A subsystem is a module mounted as an isolated graph. Each file in yatta/func/ is one.
import { createRuntime, defineSubsystem } from "yatta/runtime"; const runtime = createRuntime({ taskTimeoutMs: 30_000 });await runtime.start(); const graphId = await runtime.registerSubsystem( defineSubsystem({ name: "billing", entrypoint: new URL("./func/billing.ts", import.meta.url), workload: "cpu", }),); // Dispatch a named export from that module.const receipt = await runtime.execute(graphId, "computeReceipt", payload);namestringentrypointURL | stringworkload"cpu" | "io"envRecord<string, string>timeoutMsnumberChoosing the workload
// CPU-bound: Argon2id, AES, image work, large JSON transformsdefineSubsystem({ name: "auth", workload: "cpu", ... }) // I/O-bound: SQLite, fetch, mail, S3defineSubsystem({ name: "db", workload: "io", ... })cpu is how you tell the scheduler it may block. Getting it wrong is the usual cause of “my server freezes under load”.Dispatch
The scheduler picks the least-loaded worker for the graph. When every slot is busy the task waits in a fast queue and is drained as capacity frees up.
// Fire and forgetvoid runtime.execute(graphId, "sendReport", { id: 1 }); // Awaitconst result = await runtime.execute(graphId, "computeReceipt", payload); // Concurrent calls share the workersawait Promise.all( orders.map((o) => runtime.execute(graphId, "computeReceipt", o)),);Micro-batching
Under a backlog the scheduler stops sending one message per task and packs up to 32 into a single IPC message (64 past 256 queued). This removes most of the thread-hop cost under burst load without changing your code.
Inspecting the fleet
console.log(runtime.getTopology());// {// cpuCores: 2,// cpuWorkers: 1,// ioWorkers: 2,// totalWorkers: 3,// suggestSmol: true// } console.log(runtime.getActiveTaskCount()); // in flight + queuedEnvironment
Each subsystem can override the scheduler-wide deadline:
const runtime = createRuntime({ cpuWorkers: 2, // override the computed default ioWorkers: 4, taskTimeoutMs: 30_000, silent: false, // suppress the startup banner}); // Per-subsystemdefineSubsystem({ name: "exports", entrypoint: "./func/exports.ts", workload: "cpu", timeoutMs: 120_000, // a long report gets more time});timeoutMs: 0 disables the deadline for that subsystem. Only do this for work you control — see Reliability for what an unbounded task costs.