Skip to main content
Bun implements the Web Workers API for running JavaScript in parallel threads. Workers are useful for CPU-intensive tasks, parallel processing, and isolating code execution.

Creating a Worker

const worker = new Worker("worker.ts");

worker.postMessage({ task: "compute", data: [1, 2, 3] });

worker.onmessage = (event) => {
  console.log("Result:", event.data);
};
In worker.ts:
self.onmessage = (event) => {
  const { task, data } = event.data;
  
  if (task === "compute") {
    const result = data.reduce((a, b) => a + b, 0);
    self.postMessage(result);
  }
};

Worker Options

Module Type

Workers can be ES modules or classic scripts:
// ES Module (default in Bun)
const worker = new Worker("worker.ts", { type: "module" });

// Classic script
const worker = new Worker("worker.js", { type: "classic" });

Worker Name

Name workers for debugging:
const worker = new Worker("worker.ts", { name: "DataProcessor" });

console.log(worker.name); // "DataProcessor"

Credentials

Control credential passing for module imports:
const worker = new Worker("worker.ts", {
  credentials: "same-origin", // or "include", "omit"
});

Communication

Posting Messages

Send data between main thread and worker:
// Main thread
worker.postMessage({ type: "start", config: { threads: 4 } });

// Worker
self.onmessage = (event) => {
  const { type, config } = event.data;
  // Process message
};

Message Event

worker.addEventListener("message", (event) => {
  console.log("Received:", event.data);
});

// Or using property
worker.onmessage = (event) => {
  console.log("Received:", event.data);
};

Error Handling

worker.addEventListener("error", (event) => {
  console.error("Worker error:", event.message);
  console.error("File:", event.filename);
  console.error("Line:", event.lineno);
});

worker.onerror = (event) => {
  console.error("Error in worker:", event.message);
};

Unhandled Rejections

worker.addEventListener("messageerror", (event) => {
  console.error("Message deserialization failed");
});

Transferring Data

Structured Clone

By default, data is cloned:
const data = { numbers: [1, 2, 3], text: "hello" };
worker.postMessage(data);
// `data` is cloned and can still be used
Supported types:
  • Primitives (string, number, boolean)
  • Objects and Arrays
  • Date, RegExp, Map, Set
  • ArrayBuffer, TypedArrays
  • Blob, File, ImageData

Transfer Objects

For better performance, transfer ownership:
const buffer = new ArrayBuffer(1024 * 1024); // 1MB

// Transfer ownership to worker
worker.postMessage({ buffer }, [buffer]);

// buffer is now detached and unusable in main thread
console.log(buffer.byteLength); // 0
Transferable types:
  • ArrayBuffer
  • MessagePort
  • ReadableStream
  • WritableStream
  • TransformStream

Shared Memory

Use SharedArrayBuffer for shared memory:
// Main thread
const shared = new SharedArrayBuffer(1024);
const view = new Int32Array(shared);

worker.postMessage({ shared });

// Both threads can access the same memory
view[0] = 42;

// Worker thread
self.onmessage = ({ data }) => {
  const view = new Int32Array(data.shared);
  console.log(view[0]); // 42
  view[0] = 100;
};

Worker Lifecycle

Terminating Workers

// Graceful shutdown
worker.postMessage({ type: "shutdown" });

// Force terminate
worker.terminate();
Inside the worker:
self.onmessage = (event) => {
  if (event.data.type === "shutdown") {
    // Cleanup
    self.close(); // Terminate from inside
  }
};

Reference Counting

Control whether worker keeps process alive:
// Worker keeps process alive (default)
worker.ref();

// Worker doesn't keep process alive
worker.unref();

Worker Scope

Global Objects

Inside a worker:
console.log(self); // WorkerGlobalScope
console.log(self.name); // Worker name if provided

// No window or document
console.log(typeof window); // undefined
console.log(typeof document); // undefined

Available APIs

Workers have access to:
  • console
  • setTimeout, setInterval, setImmediate
  • fetch, WebSocket
  • crypto, TextEncoder, TextDecoder
  • URL, URLSearchParams
  • Blob, File, FormData
  • ReadableStream, WritableStream
  • Import maps and ES modules

Not Available

  • DOM APIs (document, window)
  • Most Bun-specific APIs (Bun.serve, etc.)
  • Process-level APIs (process.exit)

Import Maps in Workers

Workers support import maps:
// Main thread
const worker = new Worker("worker.ts", {
  type: "module",
});

// worker.ts can use imports
import { helper } from "./utils.ts";
import lodash from "lodash";

TypeScript

Bun automatically transpiles TypeScript in workers:
// worker.ts
interface Task {
  id: number;
  payload: unknown;
}

self.onmessage = (event: MessageEvent<Task>) => {
  const { id, payload } = event.data;
  // Type-safe code
};

Nested Workers

Workers can create other workers:
// worker.ts
const subWorker = new Worker("sub-worker.ts");

subWorker.postMessage({ task: "process" });

subWorker.onmessage = (event) => {
  // Forward result to main thread
  self.postMessage(event.data);
};

Common Patterns

Worker Pool

class WorkerPool {
  workers: Worker[] = [];
  queue: Array<{ data: any; resolve: Function }> = [];
  
  constructor(size: number, script: string) {
    for (let i = 0; i < size; i++) {
      const worker = new Worker(script);
      worker.onmessage = (event) => {
        const next = this.queue.shift();
        if (next) {
          worker.postMessage(next.data);
        }
      };
      this.workers.push(worker);
    }
  }
  
  async run(data: any): Promise<any> {
    return new Promise((resolve) => {
      const worker = this.workers.find(w => /* is idle */);
      if (worker) {
        worker.postMessage(data);
      } else {
        this.queue.push({ data, resolve });
      }
    });
  }
  
  terminate() {
    this.workers.forEach(w => w.terminate());
  }
}

const pool = new WorkerPool(4, "worker.ts");

for (let i = 0; i < 100; i++) {
  await pool.run({ task: i });
}

pool.terminate();

Request-Response Pattern

// Main thread
let messageId = 0;
const pending = new Map();

worker.onmessage = (event) => {
  const { id, result, error } = event.data;
  const { resolve, reject } = pending.get(id);
  pending.delete(id);
  
  if (error) reject(error);
  else resolve(result);
};

function request(data: any): Promise<any> {
  return new Promise((resolve, reject) => {
    const id = messageId++;
    pending.set(id, { resolve, reject });
    worker.postMessage({ id, data });
  });
}

const result = await request({ type: "compute", value: 42 });

// Worker
self.onmessage = async (event) => {
  const { id, data } = event.data;
  
  try {
    const result = await processData(data);
    self.postMessage({ id, result });
  } catch (error) {
    self.postMessage({ id, error: error.message });
  }
};

Progress Updates

// Worker
self.onmessage = async (event) => {
  const total = event.data.items.length;
  
  for (let i = 0; i < total; i++) {
    await processItem(event.data.items[i]);
    
    // Send progress
    self.postMessage({
      type: "progress",
      current: i + 1,
      total,
    });
  }
  
  self.postMessage({ type: "complete" });
};

// Main thread
worker.onmessage = (event) => {
  if (event.data.type === "progress") {
    const pct = (event.data.current / event.data.total) * 100;
    console.log(`Progress: ${pct.toFixed(1)}%`);
  } else if (event.data.type === "complete") {
    console.log("Done!");
  }
};

Performance Considerations

When to Use Workers

Good use cases:
  • CPU-intensive computations
  • Parsing large data files
  • Image/video processing
  • Cryptographic operations
  • Parallel data processing
Not ideal for:
  • I/O operations (use async instead)
  • Simple computations (overhead not worth it)
  • DOM manipulation (not available in workers)

Minimizing Communication

Message passing has overhead:
// Bad - many small messages
for (let i = 0; i < 1000; i++) {
  worker.postMessage({ index: i });
}

// Good - batch messages
worker.postMessage({ items: Array.from({ length: 1000 }, (_, i) => i) });

Using Transfers

Transfer large buffers instead of cloning:
// Slow - clones 100MB
const data = new Uint8Array(100 * 1024 * 1024);
worker.postMessage({ data });

// Fast - transfers ownership
worker.postMessage({ data }, [data.buffer]);

Debugging Workers

// Named workers help identify threads
const worker = new Worker("worker.ts", { name: "ImageProcessor" });

// Use console.log in workers
self.onmessage = (event) => {
  console.log("Worker received:", event.data);
};

// Catch errors
worker.onerror = (event) => {
  console.error(`Error in ${event.filename}:${event.lineno}`);
  console.error(event.message);
};

Compatibility

Bun’s Web Workers are compatible with:
  • Browsers (Chrome, Firefox, Safari, Edge)
  • Deno
  • Cloudflare Workers (with some limitations)

Differences from Node.js

Bun implements the standard Web Workers API, not Node.js worker_threads:
// Bun (Web Workers)
const worker = new Worker("worker.ts");

// Node.js (worker_threads)
const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js');
Migration tips:
  • Use postMessage instead of parentPort.postMessage
  • Use self.onmessage instead of parentPort.on('message')
  • No workerData - pass data via postMessage

Build docs developers (and LLMs) love