Skip to content

Why Knitting

The first time one function gets expensive, the usual advice is to pull it out into a service. That is a lot of machinery for one function.

A parser grows. A validator picks up rules. A hash, render, or compression step starts taking more of the event loop than it should. Nothing else about the app has changed: one function got heavy, and the fix on offer is new infrastructure.

Knitting is for that gap. Keep the code where it is, move the work off the main thread.

CPU pressure in a JavaScript app rarely spreads evenly. Real codebases often have thousands of functions, but only a few of them create most of the cost.

That changes the size of the fix. When the cost is concentrated in a handful of functions, the response can be concentrated too — moving the expensive part does not have to mean moving the application around it.

When the main thread starts paying for those functions, JavaScript developers usually reach for one of three options.

Do nothing. It is simple, and for a while it works. But every millisecond a hot function spends on the main thread is a millisecond the server cannot spend on everything else. Cheap routes queue behind expensive ones. Scaling out dilutes the pain, but it does not remove the hot path from the event loop.

Workers. Closer to the mark: the work leaves the main thread and stays on the machine. What makes it painful is everything around it. The runtime hands you postMessage and leaves the rest to you — request IDs, routing, errors, promises, lifecycle, payload choices. That plumbing is the original reason Knitting exists.

Make it a service. Sometimes that is the right call. Separate ownership, deploy cadence, and failure domains are all real reasons to cross the network. But for one hot function, owned by the same team, in the same repo, the bill is strange: a network hop, serialization, deployment, monitoring, and one more thing to operate. The function did not ask for a hostname. It asked for somewhere else to run.

Knitting adds a smaller boundary: the code stays where it is, and only the execution moves.

The function stays in your repo, in your language, in your types — one import away from its caller. What changes is where it runs and what it can touch. You export it, hand it to a pool, and call it like the async function it already was. Underneath, it runs on a real thread or an isolated process.

import { createPool, isMain } from "knitting";
export const hello = (name: string) => "Hello " + name;
if (isMain) {
using pool = createPool({})({ hello });
console.log(await pool.call.hello("World!"));
}

That is the whole boundary: one export, one pool, one call. hello still reads like a plain function, but it no longer runs on the main thread.

Call that a function-level execution boundary: a way to move the expensive part without moving the whole app.

Compare what each option costs for the same few hot functions:

In-processHand-rolled workersMicroserviceKnitting
Main thread protectednoyesyesyes
Code stays in the appyesmostlynoyes
Call-site ergonomicsfunction callprotocol you wroteHTTP clientfunction call
Transport costnonepostMessage per callnetwork + JSONshared-memory mailboxes
Isolation availablenonethread onlyfull, always-ona dial, per pool
New deploy unitnonoyesno

Two things make that boundary worth having.

Transport cost matters. Workers can feel disappointing when reaching the worker costs more than the work. Knitting uses shared-memory mailboxes instead of the runtime’s message queue, so small and medium calls stay practical. The Architecture page explains the mechanism, and the benchmarks show where the shape wins and where it does not.

Isolation should match the task. A microservice gives you process isolation whether or not you need it. Knitting lets each pool pick its own boundary: in-process guards, a bootstrap hook, runtime-native permissions, or a real OS-sandboxed process. Trusted math can stay on cheap threads. An untrusted plugin can run behind bwrap, and importTask keeps its code off the host entirely.

Knitting does not replace distributed systems. If you need cross-machine scale, separate failure domains, or team-level ownership boundaries, you still need the network. Knitting is for the moment before that, when the code belongs in the app but the work no longer belongs on the main thread.

Knitting today is task-call oriented: one request in, one response out. The transport underneath it is more general than that. Mailboxes, payload buffers, and named mappings leave room for other shapes.

01Nearer term

Channels

Some same-host work is not a task call: progress, long-lived coordination, producer/consumer flows. Today the honest answer is MessagePort.

A future channel API would keep that shape on the same shared-memory transport, without pretending every conversation is request/response.

02Longer term

Cross-language, same-host

The mailbox protocol is not tied to JavaScript. Process workers already open named mappings, which makes the boundary more about memory than a specific runtime.

A later version could let JavaScript hand large payloads to another runtime on the same machine without copying through JSON or re-marshalling at every hop.

That is why the transport is built with more care than a worker pool strictly needs. Until those APIs ship, though, judge Knitting on what it does now.

  • Quick Start — the ten-line version of everything argued above.
  • Architecture — the mailbox transport, lane model, and safety layers in detail.
  • Process workers — the hard end of the isolation dial: sandboxes and containers.
  • Examples — the hot functions behind this argument, as copyable code.