# Workers (/runtime/workers)

<!-- agent-signals: reading_time_min: 8 · est_tokens: 3099 · updated: 2026-09-23 -->
Related: [Watch Mode](/runtime/watch-mode.md), [Debugging](/runtime/debugger.md), [REPL](/runtime/repl.md), [bunfig.toml](/runtime/bunfig.md), [File Types](/runtime/file-types.md), [Module Resolution](/runtime/module-resolution.md)

<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` [#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 [#from-the-main-thread]

```ts icon="/icons/typescript.svg" title="index.ts"
const worker = new Worker("./worker.ts");

worker.postMessage("hello");
worker.onmessage = event => {
  console.log(event.data);
};
```

### Worker thread [#worker-thread]

```ts icon="file-code" title="worker.ts"
// 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 [#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 icon="/icons/typescript.svg" title="index.ts"
const worker = new Worker("./worker.ts", {
  preload: ["./load-sentry.js"],
});
```

You can also pass a single string to the `preload` option:

```ts icon="/icons/typescript.svg" title="index.ts"
const worker = new Worker("./worker.ts", {
  preload: "./load-sentry.js",
});
```

### `blob:` URLs [#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"` [#open]

Bun emits the `"open"` event when a worker is created and ready to receive messages. (This event does not exist in browsers.)

```ts icon="/icons/typescript.svg" title="index.ts"
const worker = new Worker(new URL("worker.ts", import.meta.url).href);

worker.addEventListener("open", () => {
  console.log("worker is ready");
});
```

Messages posted before the worker's script has run are queued until it has, so you don't need to wait for the `"open"` event before sending. They are delivered to the handler the script installed while it ran. If it installed none, they are dropped.

As in browsers, a message is dispatched to the worker as soon as the synchronous part of its script has run, whether or not a `"message"` handler exists yet, and a message dispatched while there is no handler is dropped. Install `self.onmessage` / `self.addEventListener("message", ...)` before the first top-level `await`:

```ts icon="file-code" title="worker.ts"
declare var self: Worker;

// installed before any `await`: nothing posted during startup is missed
self.onmessage = (event: MessageEvent) => {
  console.log(event.data);
};

const config = await loadConfig();
```

A top-level `await` in a statically imported module counts too. Static imports are evaluated before the worker's own code, so if any module in the import graph awaits at its top level, delivery starts before the first line of the worker runs and installing the handler on that first line is already too late. Load such modules with a dynamic `import()` after installing the handler, or use `node:worker_threads`.

[`node:worker_threads`](https://nodejs.org/api/worker_threads.html) differs here, in Bun as in Node.js: `parentPort` keeps messages queued until a `"message"` listener is attached (and again whenever the last one is removed), so a worker that runs `await init()` before `parentPort.on("message", ...)` still receives everything in order.

## Messages with `postMessage` [#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 [#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 [#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 icon="/icons/typescript.svg" title="index.ts"
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()` [#processexit]

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"` [#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 icon="/icons/typescript.svg" title="index.ts"
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 [#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()` [#workerunref]

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 icon="/icons/typescript.svg" title="index.ts"
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.unref();
```

`worker.unref()` is not available in browsers.

### `worker.ref()` [#workerref]

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 icon="/icons/typescript.svg" title="index.ts"
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 icon="/icons/typescript.svg" title="index.ts"
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` [#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 icon="/icons/typescript.svg" title="index.ts"
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 [#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 [#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` [#bunismainthread]

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");
}
```
