The Worker API is still experimental, particularly around worker termination. Bun is actively working on improving this.
Worker lets you start and communicate with a new JavaScript instance running on a separate thread, while sharing I/O resources with the main thread.
Bun implements the Web Workers API with server-side extensions. Like the rest of Bun, workers support CommonJS, ES Modules, TypeScript, JSX, and TSX out of the box — no build step needed.
Creating a worker
Main thread
const worker = new Worker("./worker.ts");
worker.postMessage({ type: "start", payload: "hello" });
worker.onmessage = event => {
console.log(event.data);
};
Worker thread
declare var self: Worker;
self.onmessage = (event: MessageEvent) => {
console.log(event.data);
postMessage("world");
};
Add declare var self: Worker; to the top of your worker file to avoid TypeScript errors. Unlike in browsers, you do not need to specify { type: "module" } to use ES Modules in workers.
The specifier passed to new Worker(url) is resolved relative to the project root. If the file does not exist, an error is thrown immediately at construction time.
Sending and receiving messages
Use worker.postMessage to send messages to the worker, and self.postMessage inside the worker to send messages back. Messages are serialized using the HTML Structured Clone Algorithm.
// Main thread → worker
worker.postMessage({ command: "process", data: [1, 2, 3] });
// Inside the worker → main thread
postMessage({ result: "done" });
Listen for messages using event listeners:
// Worker thread
self.addEventListener("message", event => {
console.log(event.data);
});
// Main thread
worker.addEventListener("message", event => {
console.log(event.data);
});
Bun includes fast paths for postMessage that dramatically improve performance for common data types:
- String fast path — posting a pure string bypasses structured clone entirely.
- Simple object fast path — plain objects containing only primitives use an optimized path.
With these fast paths, Bun’s postMessage is 2–241x faster than Node.js for typical payloads.
// Optimized: string fast path
postMessage("Hello, worker!");
// Optimized: simple object fast path
postMessage({ message: "Hello", count: 42, enabled: true });
// Standard structured clone (nested objects, Dates, buffers, etc.)
postMessage({ nested: { deep: true }, date: new Date() });
Worker lifecycle events
”open”
Emitted when the worker is ready to receive messages. Messages sent before this event are automatically queued.
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.addEventListener("open", () => {
console.log("Worker is ready");
});
”close”
Emitted when the worker has been terminated. The CloseEvent contains the exit code.
worker.addEventListener("close", event => {
console.log("Worker closed with code:", event.code);
});
Terminating workers
A worker terminates automatically when its event loop has no more work. To forcefully terminate a worker:
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
// Some time later...
worker.terminate();
A worker can also terminate itself using process.exit(). This does not terminate the main process.
Managing worker lifetime
By default, an active Worker keeps the main process alive.
worker.unref()
Decouple the worker’s lifetime from the main process. The main process can exit even if the worker is still running:
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.unref();
You can also set this via the constructor:
const worker = new Worker(new URL("worker.ts", import.meta.url).href, {
ref: false,
});
worker.ref()
Re-attach the worker to the main process lifetime:
worker.unref();
// later...
worker.ref();
worker.ref() and worker.unref() are Bun extensions and are not available in browsers.
Checking the current thread
Use Bun.isMainThread to check whether code is running on the main thread or in a worker:
if (Bun.isMainThread) {
console.log("Running on the main thread");
} else {
console.log("Running in a worker");
}
Sharing environment data
Share data from the main thread to all workers using setEnvironmentData and getEnvironmentData from worker_threads:
import { setEnvironmentData } from "worker_threads";
setEnvironmentData("config", { apiUrl: "https://api.example.com" });
const worker = new Worker("./worker.ts");
import { getEnvironmentData } from "worker_threads";
const config = getEnvironmentData("config");
console.log(config); // => { apiUrl: "https://api.example.com" }
Preloading modules
Use the preload option to load modules before the worker script starts. This is useful for instrumentation tools like OpenTelemetry, Sentry, or DataDog:
const worker = new Worker("./worker.ts", {
preload: ["./load-sentry.js"],
});
blob: URLs
You can create a worker from an inline string using a blob: URL:
const code = `
self.onmessage = (event) => {
postMessage(event.data * 2);
};
`;
const blob = new Blob([code], { type: "application/typescript" });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.postMessage(21);
worker.onmessage = e => console.log(e.data); // 42
Workers created from blob: URLs support TypeScript, JSX, and other formats out of the box.
Memory usage
Workers can consume significant memory. Use smol: true to reduce memory usage at the cost of some performance:
const worker = new Worker("./worker.ts", {
smol: true,
});
This sets the JSC heap size to Small instead of Large.
Worker events on the main process
Listen for worker creation events using process.on("worker"):
process.on("worker", worker => {
console.log("New worker created, thread ID:", worker.threadId);
});
SharedArrayBuffer and Atomics
For true shared memory between threads, use SharedArrayBuffer with Atomics for synchronization:
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);
const worker = new Worker("./worker.ts");
worker.postMessage({ buffer: sharedBuffer });
// Wait until the worker signals completion
Atomics.wait(sharedArray, 0, 0);
console.log("Worker wrote:", sharedArray[0]);
declare var self: Worker;
self.onmessage = (event: MessageEvent) => {
const sharedArray = new Int32Array(event.data.buffer);
// Write a value and notify the main thread
Atomics.store(sharedArray, 0, 42);
Atomics.notify(sharedArray, 0, 1);
};
Atomics.wait() blocks the thread. Never call it on the main thread in a browser context. In Bun server-side code, use it only in worker threads or when blocking is intentional.
Node.js worker_threads compatibility
Bun is compatible with Node.js’s worker_threads module:
import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
if (isMainThread) {
const worker = new Worker(__filename, {
workerData: { value: 42 },
});
worker.on("message", result => {
console.log("Result:", result); // 84
});
} else {
parentPort?.postMessage(workerData.value * 2);
}