---
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, an `"error"` event is emitted on the `Worker` object.

```js
const worker = new Worker("/not-found.js");
worker.addEventListener("error", event => {
  console.log(event.message);
});
```

The specifier passed to `Worker` is resolved 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"`

The `"open"` event is emitted 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/Window/postMessage). Messages are serialized 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 (and not on the main thread), and the exit code is passed to the `"close"` event.

### `"close"`

The `"close"` event is emitted 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 an `options` object to `Worker`:

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