Skip to content

Why Knitting

Most apps do not need a new service the first time one function gets expensive. They need a better place for that function to run.

A parser gets large. A validator gets complicated. A hash, render, or compression step starts taking more of the event loop than it should. The rest of the app is still fine. The problem is smaller than the architecture we often reach for.

Knitting is built for that middle place: keep the code local, but 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 matters because it changes the size of the fix. You may not have an application problem. You may have a function problem. If the heat is local, the solution should be local too.

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. The shape is closer: move the work somewhere else on the same machine. The rough part is everything around it. The runtime gives you postMessage; the rest is yours: 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:

Keep the code local. Move the execution boundary.

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;
using pool = createPool({})({ hello });
if (isMain) 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.

So Knitting is a function-level execution boundary. Not a new service, and not just a worker helper — 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 details make the fourth shape useful.

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.

Not every hot path deserves a hostname.

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.