Skip to content

Quick Start

This quick start walks the whole loop: define tasks, create a pool, call tasks like async functions, and let the pool shut down cleanly with using.

In a few minutes you’ll have your first tasks running in parallel.
If you already know what workers are, think of Knitting as: “workers, but with a function-call API and much lower overhead.”

A few words we’ll use a lot:

  • Task: a function the workers can run. The simplest task is just an exported function; wrap it with task({ f }) when you want options like timeouts or aborts.
  • call.*(): runs a task on the pool and returns a Promise with the result.
  • isMain: a boolean that’s true only on the host. Workers re-import your module, so guard host-only code — like creating the pool — with it.
  • createPool(): starts the workers and returns a typed call object plus shutdown. The pool is also disposable, so using can close it for you.
  • Host ↔ Worker: the host is the process that creates the pool; workers are the threads (or processes) that run the tasks.

Now that we share the vocabulary, the snippets should feel familiar.

hello_world.ts
import { createPool, isMain } from "knitting";
// A task is just an exported function the workers can run.
export const greet = (name: string) => `hello ${name}`;
if (isMain) {
// `using` shuts the pool down automatically when this block ends.
using pool = createPool({ threads: 1 })({ greet });
console.log(await pool.call.greet("knitting")); // hello knitting
}
  1. Import what you need:

    import { createPool, isMain } from "knitting";
  2. Export your tasks. The simplest task is just an exported function, kept at module scope so workers can load it by name:

    export const square = (n: number) => n * n;
    export const greet = (name: string) => `hello ${name}`;
  3. Create the pool inside an isMain guard. Workers re-import this module, and the guard keeps them from re-running your host code:

    if (isMain) {
    using pool = createPool({ threads: 2 })({ square, greet });
    }

    using makes the pool close itself when the block ends — no manual cleanup.

  4. Call tasks like async functions. They’re just promises, so batch them with Promise.all:

    if (isMain) {
    using pool = createPool({ threads: 2 })({ square, greet });
    const [n, message] = await Promise.all([
    pool.call.square(8),
    pool.call.greet("knitting"),
    ]);
    console.log({ n, message }); // { n: 64, message: "hello knitting" }
    }
  5. Shut down when you’re done.

    With using, that already happens at the end of the block: workers stop and the process can exit. When you need to control the timing — or you’re on a runtime without using — call shutdown() yourself:

    const pool = createPool({ threads: 2 })({ square, greet });
    try {
    console.log(await pool.call.square(8));
    } finally {
    await pool.shutdown();
    }

For a quick script with a single task, skip the separate createPool call:

import { isMain, task } from "knitting";
export const double = task({
f: (n: number) => n * 2,
}).createPool({ threads: 2 });
if (isMain) {
try {
console.log(await double.call(21)); // 42
} finally {
await double.shutdown();
}
}

A few things that keep Knitting code clean and fast.

It keeps your app code tidy and lets workers load only what they need.

  • package.json

  • deno.json

  • Directory

    src

    • Directory

      knitting

      • database.ts
      • img_parsing.ts
      • jwt.ts
    • Directory

      app/

    • Directory

      pages/

call.*() accepts Promise<supported> inputs. Knitting resolves them on the host before dispatch, so unresolved promise state never crosses the thread boundary. That is handy in request handlers — you can pass a body read directly:

app.post("/validate", async (c) => {
const result = await pool.call.validate(c.req.text());
return c.json(result);
});

If the input promise rejects, the host call rejects and the worker never runs.

Smaller, flatter values move fastest: numbers, booleans, and short strings, then typed arrays and Buffer, then compact JSON. For bytes-plus-metadata, use Envelope. See Supported payloads.

Worker permissions are restricted by default — sensitive paths like .env, .git, ~/.ssh, and /etc are blocked, and node_modules is deny-write. Open only what your tasks actually need. See Permissions.

The handful of things that trip people up most — most of them follow from one fact: workers re-import the module that defines your tasks.

  • One argument per task. pool.call.add(a, b) won’t work — pass a tuple or object: pool.call.add([a, b]) for ([a, b]) => a + b.
  • Guard host code with isMain. Without it, pool creation (and any other host-only code) re-runs inside every worker.
  • Top-level imports run in every worker. import is hoisted, so it executes before any isMain check. Keep tasks in their own lean module so workers don’t load your whole server framework.
  • Export your tasks. An unexported task() / importTask() is invisible to the worker loader, so the call just hangs — no handler is ever registered.
  • importTask targets are plain functions, not task() wrappers (that throws a TypeError). Put timeout / abortSignal options on the importTask call instead.
  • Worker console.* is silent by default in strict mode. Pass permission: { console: true } to surface worker logs.
  • Can’t tell what the pool is doing? Pass debug: true to createPool (or set the KNITTING_DEBUG=* env var) to stream setup, import, and lifecycle diagnostics to stderr — each line tagged with the worker and a millisecond timer. Narrow the noise with namespaces: debug: { host: true, imports: true }. It costs nothing when off.
  • Only supported payloads cross the boundary. Map, Set, class instances, and functions are rejected — see Payloads.
  • Dynamic payloads cap at ~8 MiB by default; raise payload.maxPayloadBytes (and payload.payloadMaxByteLength) for larger ones.

Going further: Knitting can also run each worker as a separate process — even inside a bwrap sandbox or a container — for stronger isolation (see Process workers), and move large buffers with ProcessSharedBuffer (shared memory, no copy — see Shared memory).