Parallel work with Bun's Worker
Published May 21, 2027
export const countPrimesOnWorker = (from: number, to: number) => {
const workerCode = `
self.onmessage = ({ data: [from, to] }) => {
let count = 0;
for (let n = from; n < to; n++) {
let isPrime = n > 1;
for (let d = 2; d * d <= n; d++) if (n % d === 0) { isPrime = false; break; }
if (isPrime) count++;
}
postMessage(count);
};
`;
const worker = new Worker(URL.createObjectURL(new Blob([workerCode], { type: "application/javascript" })));
return new Promise<number>((resolve) => {
worker.onmessage = (event) => {
resolve(event.data);
worker.terminate();
};
worker.postMessage([from, to]);
});
};
const ranges: [number, number][] = [[0, 250_000], [250_000, 500_000], [500_000, 750_000], [750_000, 1_000_000]];
const counts = await Promise.all(ranges.map(([from, to]) => countPrimesOnWorker(from, to)));
console.log(`primes below 1,000,000: ${counts.reduce((a, b) => a + b, 0)}`);What
Bun implements the standard Worker API (the same one browsers expose) backed by real OS
threads. new Worker(url) starts one; postMessage/onmessage move data across the thread
boundary, and — same as in a browser — a blob: URL can hand a worker its code as an in-memory
string instead of a separate file.
Why it matters
JavaScript's single-threaded event loop is fine for I/O-bound work but a real bottleneck for
CPU-bound work — a tight loop blocks everything else on that thread, event loop included. Bun's
Worker moves that loop onto its own OS thread, and unlike browsers, Bun's version doesn't
require { type: "module" } to use import/export, and its postMessage has fast paths for
plain strings and simple objects that make it 2-241x faster than Node's worker_threads
equivalent for typical payloads. Splitting one CPU-bound task across several workers lets it use
more than one core, something no amount of async/await on a single thread can do.
How it works
runOnWorkerbuilds each worker's entire source as a template string and wraps it in aBlob, thenURL.createObjectURL(...)turns that blob into a URLWorkercan load — this keeps the whole example in one file instead of needing a separateworker.tsalongside it.- Inside the worker string,
self.onmessagereceives whatever the main thread posts — here, a[from, to]range — and the worker counts primes in that slice with an ordinary nested loop. postMessage(count)inside a worker is automatically routed to the parent; the main thread'sworker.onmessagereceives it and resolves the wrappingPromise.- Four workers each get a disjoint slice of the 0-1,000,000 range;
Promise.allwaits for all four, and the counts are summed once every worker has reported back. worker.terminate()runs right after the result arrives — without it, each worker would keep its own event loop alive indefinitely waiting for more messages that never come.
Gotchas
- A real project should give each worker its own
worker.tsfile and load it withnew Worker(new URL("./worker.ts", import.meta.url))instead of a blob string — the blob approach here trades that clarity for keeping this example runnable as a single file. - Forgetting
worker.terminate()(or never letting the worker's event loop go idle) leaks a thread per call —Bun.spawn-style cleanup discipline applies here too. postMessageuses the structured clone algorithm for anything beyond a string or simple flat object — functions, class instances with methods, and DOM-like objects either fail to clone or lose their prototype, so pass plain data across the boundary, not behavior.
Related
Bun.spawn— a separate process with its own memory space, appropriate when the work needs isolation (a crash in one doesn't take down the others) rather than just parallelism.node:worker_threads'sWorkeris API-compatible in Bun for code already written against Node's version; the standardWorkerglobal shown here is Bun's preferred, browser-aligned API.