Files
bun-src/docs/runtime/workers.mdx
T
2026-08-27 21:09:14 +00:00

313 lines
10 KiB
Plaintext

---
title: Workers
description: Use Bun's Workers API to create and communicate with a new JavaScript instance running on a separate thread while sharing I/O resources with the main thread
---
<Warning>
The `Worker` API is still experimental (particularly for terminating workers). We are actively working on improving
this.
</Warning>
With [`Worker`](https://developer.mozilla.org/en-US/docs/Web/API/Worker), 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 a minimal version of the [Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) with extensions that make it work better for server-side use cases. Like the rest of Bun, `Worker` supports CommonJS, ES modules, TypeScript, JSX, and TSX with no extra build step.
## Creating a `Worker`
Like in browsers, [`Worker`](https://developer.mozilla.org/en-US/docs/Web/API/Worker) is a global. Use it to create a new worker thread.
### From the main thread
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker("./worker.ts");
worker.postMessage("hello");
worker.onmessage = event => {
console.log(event.data);
};
```
### Worker thread
```ts worker.ts icon="file-code"
// prevents TS errors
declare var self: Worker;
self.onmessage = (event: MessageEvent) => {
console.log(event.data);
postMessage("world");
};
```
To prevent TypeScript errors when using `self`, add this line to the top of your worker file.
```ts
declare var self: Worker;
```
You can use `import` and `export` syntax in your worker code. Unlike in browsers, you don't need to pass `{type: "module"}` to use ES modules.
If the worker's script fails to resolve, Bun emits an `"error"` event on the `Worker` object.
```js
const worker = new Worker("/not-found.js");
worker.addEventListener("error", event => {
console.log(event.message);
});
```
Bun resolves the specifier passed to `Worker` relative to the project root (like typing `bun ./path/to/file.js`).
### `preload` - load modules before the worker starts
Pass an array of module specifiers to the `preload` option to load them before the worker's own code runs, like the `--preload` CLI argument. Use it for code that must load first, such as OpenTelemetry, Sentry, or DataDog.
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker("./worker.ts", {
preload: ["./load-sentry.js"],
});
```
You can also pass a single string to the `preload` option:
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker("./worker.ts", {
preload: "./load-sentry.js",
});
```
### `blob:` URLs
You can also pass a `blob:` URL to `Worker` to create a worker from a string or other in-memory source.
```js
const blob = new Blob([`self.onmessage = (event: MessageEvent) => postMessage(event.data)`], {
type: "application/typescript",
});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
```
Like the rest of Bun, workers created from `blob:` URLs support TypeScript, JSX, and other file types. To tell Bun the source is TypeScript, set the `type` on the `Blob` or pass a `filename` to the `File` constructor.
```ts
const file = new File([`self.onmessage = (event: MessageEvent) => postMessage(event.data)`], "worker.ts");
const url = URL.createObjectURL(file);
const worker = new Worker(url);
```
### `"open"`
Bun emits the `"open"` event when a worker is created and ready to receive messages. (This event does not exist in browsers.)
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.addEventListener("open", () => {
console.log("worker is ready");
});
```
Bun enqueues messages until the worker is ready, so you don't need to wait for the `"open"` event before sending.
## Messages with `postMessage`
To send messages, use [`worker.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) and [`self.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/postMessage). Bun serializes messages with the [HTML Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).
### Performance optimizations
Bun has fast paths for `postMessage` with common data types:
**String fast path** - When posting a pure string, Bun bypasses the structured clone algorithm entirely, so there is no serialization overhead.
**Simple object fast path** - For plain objects containing only primitive values (strings, numbers, booleans, null, undefined), Bun stores properties directly without full structured cloning.
The simple object fast path activates when the object:
- Is a plain object with no prototype chain modifications
- Contains only enumerable, configurable data properties
- Has no indexed properties or getter/setter methods
- All property values are primitives or strings
With these fast paths, Bun's `postMessage` performs **2-241x faster** because the message length no longer has a meaningful impact on performance.
**Bun (with fast paths):**
```ts
postMessage({ prop: 11 chars string, ...9 more props }) - 648ns
postMessage({ prop: 14 KB string, ...9 more props }) - 719ns
postMessage({ prop: 3 MB string, ...9 more props }) - 1.26µs
```
**Node.js v24.6.0 (for comparison):**
```js
postMessage({ prop: 11 chars string, ...9 more props }) - 1.19µs
postMessage({ prop: 14 KB string, ...9 more props }) - 2.69µs
postMessage({ prop: 3 MB string, ...9 more props }) - 304µs
```
```js
// String fast path - optimized
postMessage("Hello, worker!");
// Simple object fast path - optimized
postMessage({
message: "Hello",
count: 42,
enabled: true,
data: null,
});
// Complex objects still work but use standard structured clone
postMessage({
nested: { deep: { object: true } },
date: new Date(),
buffer: new ArrayBuffer(8),
});
```
```js
// On the worker thread, `postMessage` is automatically "routed" to the parent thread.
postMessage({ hello: "world" });
// On the main thread
worker.postMessage({ hello: "world" });
```
To receive messages, use the [`message` event handler](https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event) on the worker and main thread.
```js
// Worker thread:
self.addEventListener("message", event => {
console.log(event.data);
});
// or use the setter:
// self.onmessage = fn
// if on the main thread
worker.addEventListener("message", event => {
console.log(event.data);
});
// or use the setter:
// worker.onmessage = fn
```
## Terminating a worker
A `Worker` instance terminates automatically once its event loop has no work left to do. Attaching a `"message"` listener on the global or any `MessagePort`s keeps the event loop alive. To forcefully terminate a `Worker`, call `worker.terminate()`.
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
// ...some time later
worker.terminate();
```
Calling `worker.terminate()` makes the worker exit as soon as possible.
### `process.exit()`
A worker can terminate itself with `process.exit()`. This does not terminate the main process. Like in Node.js, `process.on('beforeExit', callback)` and `process.on('exit', callback)` are emitted on the worker thread, not on the main thread. Bun passes the exit code to the `"close"` event.
### `"close"`
Bun emits the `"close"` event when a worker has been marked as terminated. The worker itself can take some time to fully exit. The `CloseEvent` contains the exit code passed to `process.exit()`, or 0 if it closed for another reason.
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.addEventListener("close", event => {
console.log("worker is being closed");
});
```
This event does not exist in browsers.
## Managing lifetime
By default, an active `Worker` keeps the main (spawning) process alive, so async tasks like `setTimeout` and promises keep the process alive. Attaching `message` listeners also keeps the `Worker` alive.
### `worker.unref()`
To stop a running worker from keeping the process alive, call `worker.unref()`. This decouples the worker's lifetime from the main process's, matching the behavior of Node.js' `worker_threads`.
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.unref();
```
`worker.unref()` is not available in browsers.
### `worker.ref()`
To keep the process alive until the `Worker` terminates, call `worker.ref()`. Workers are ref'd by default; a ref'd worker still needs something on its event loop (such as a `"message"` listener) to continue running.
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.unref();
// later...
worker.ref();
```
Alternatively, you can also pass `ref: false` in the `options` object to `Worker`, which is equivalent to calling `worker.unref()`:
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker(new URL("worker.ts", import.meta.url).href, {
ref: false,
});
```
`worker.ref()` is not available in browsers.
## Memory usage with `smol`
Bun's `Worker` supports a `smol` mode that reduces memory usage at a cost of performance. To enable it, pass `smol: true` in the `Worker` constructor's `options` object.
```ts index.ts icon="/icons/typescript.svg"
const worker = new Worker("./i-am-smol.ts", {
smol: true,
});
```
<Accordion title="What does `smol` mode actually do?">
Setting `smol: true` sets `JSC::HeapSize` to be `Small` instead of the default `Large`.
</Accordion>
## Environment Data
Share data between the main thread and workers using `setEnvironmentData()` and `getEnvironmentData()`.
```ts title="index.ts" icon="/icons/typescript.svg"
import { setEnvironmentData, getEnvironmentData } from "worker_threads";
// In main thread
setEnvironmentData("config", { apiUrl: "https://api.example.com" });
// In worker
const config = getEnvironmentData("config");
console.log(config); // => { apiUrl: "https://api.example.com" }
```
## Worker Events
Listen for worker creation events using `process.on()`:
```ts title="index.ts" icon="/icons/typescript.svg"
process.on("worker", worker => {
console.log("New worker created:", worker.threadId);
});
```
## `Bun.isMainThread`
Check `Bun.isMainThread` to tell whether you're on the main thread.
```ts
if (Bun.isMainThread) {
console.log("I'm the main thread");
} else {
console.log("I'm in a worker");
}
```