Skip to main content
Workers are the fundamental unit of execution in workerd. A worker is a piece of JavaScript or WebAssembly code that handles requests and performs computation.

What is a worker?

A worker is defined by:
  • Source code: JavaScript modules or service worker scripts
  • Configuration: Compatibility date, bindings, and other settings
  • Environment: The env object containing bindings to resources
Workers in workerd are similar to Cloudflare Workers - in fact, workerd powers the Cloudflare Workers platform.

Worker types

Service worker syntax

The original Workers syntax uses global event listeners:
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  return new Response('Hello World!');
}
Configuration:
const myWorker :Workerd.Worker = (
  serviceWorkerScript = embed "worker.js",
  compatibilityDate = "2024-01-01"
);

ES modules syntax

The modern syntax uses ES module exports:
export default {
  async fetch(request, env, ctx) {
    return new Response('Hello World!');
  }
}
Configuration:
const myWorker :Workerd.Worker = (
  modules = [
    (name = "worker.js", esModule = embed "worker.js")
  ],
  compatibilityDate = "2024-01-01"
);
The modules syntax is recommended for new projects as it:
  • Supports multiple modules and imports
  • Provides clearer separation of concerns
  • Allows for multiple entry points
  • Makes testing easier

Worker lifecycle

Understanding the worker lifecycle is crucial for writing efficient workers.

Isolate creation

When a worker script is first loaded:
  1. Isolate allocation: A V8 isolate is created (or reused if compatible)
  2. API setup: JavaScript APIs are registered
  3. Module compilation: Code is parsed and compiled

Script evaluation

The worker script executes at load time:
// This runs ONCE when the worker loads
console.log('Worker starting up');

const config = { apiKey: 'abc123' };

export default {
  // This runs for EACH request
  async fetch(request) {
    return new Response(`Using key: ${config.apiKey}`);
  }
}
You cannot perform asynchronous I/O during script evaluation. Top-level await is supported for imports, but network requests and other I/O must happen inside request handlers.

Request handling

For each incoming request:
  1. Worker instance selected: An available worker instance is chosen
  2. IoContext created: Per-request context is established
  3. Handler invoked: The fetch() handler executes
  4. Response returned: The Response is serialized and sent
  5. Cleanup: Resources are released, but the worker instance persists

Worker reuse

Worker instances are reused across requests:
let requestCount = 0;

export default {
  async fetch(request) {
    requestCount++;
    return new Response(`Request #${requestCount}`);
  }
}
State persists between requests to the same worker instance, but you cannot rely on which instance will handle a given request. For shared state, use Durable Objects or external storage.

Isolate sharing

Multiple workers with identical configuration can share a V8 isolate:
const baseWorker :Workerd.Worker = (
  modules = [(name = "base.js", esModule = embed "base.js")],
  compatibilityDate = "2024-01-01",
  bindings = [(name = "API", parameter = (type = (service = void)))]
);

const workerA :Workerd.Worker = (
  inherit = "baseWorker",
  bindings = [(name = "API", service = "apiA")]
);

const workerB :Workerd.Worker = (
  inherit = "baseWorker",
  bindings = [(name = "API", service = "apiB")]
);
Both workerA and workerB share the same isolate and compiled code, differing only in their bound services.

Bindings

Bindings give workers access to resources and services.

Service bindings

Call other workers or services:
export default {
  async fetch(request, env) {
    const response = await env.BACKEND.fetch(request);
    return response;
  }
}
Configuration:
bindings = [
  (name = "BACKEND", service = "backend-service")
]

KV namespace bindings

Access key-value storage:
export default {
  async fetch(request, env) {
    const value = await env.MY_KV.get('key');
    return new Response(value);
  }
}

Durable Object bindings

Access Durable Objects:
export default {
  async fetch(request, env) {
    const id = env.COUNTER.idFromName('global');
    const stub = env.COUNTER.get(id);
    const response = await stub.fetch(request);
    return response;
  }
}

Environment variable bindings

Provide configuration:
bindings = [
  (name = "API_KEY", text = "secret-key-123"),
  (name = "CONFIG", json = "{\"timeout\": 5000}")
]
export default {
  async fetch(request, env) {
    console.log(env.API_KEY); // "secret-key-123"
    console.log(env.CONFIG.timeout); // 5000
    return new Response('OK');
  }
}

Module types

Workers can import different types of modules:

ES modules

modules = [
  (name = "worker.js", esModule = embed "worker.js"),
  (name = "utils.js", esModule = embed "utils.js")
]
import { helper } from './utils.js';

export default {
  async fetch(request) {
    return new Response(helper());
  }
}

CommonJS modules

modules = [
  (name = "legacy.js", commonJsModule = embed "legacy.js")
]

WebAssembly modules

modules = [
  (name = "worker.js", esModule = embed "worker.js"),
  (name = "compute.wasm", wasm = embed "compute.wasm")
]
import compute from './compute.wasm';

export default {
  async fetch(request) {
    const instance = await WebAssembly.instantiate(compute);
    const result = instance.exports.calculate(42);
    return new Response(String(result));
  }
}

Text and data modules

modules = [
  (name = "worker.js", esModule = embed "worker.js"),
  (name = "template.html", text = embed "template.html"),
  (name = "data.bin", data = embed "data.bin")
]
import template from './template.html';
import data from './data.bin';

export default {
  async fetch(request) {
    return new Response(template, {
      headers: { 'Content-Type': 'text/html' }
    });
  }
}

Event handlers

Workers can export different event handlers:

fetch handler

Handles HTTP requests:
export default {
  async fetch(request, env, ctx) {
    return new Response('Hello');
  }
}

scheduled handler

Runs on a schedule (when configured externally):
export default {
  async scheduled(event, env, ctx) {
    await env.KV.put('last-run', new Date().toISOString());
  }
}

queue handler

Processes queue messages:
export default {
  async queue(batch, env, ctx) {
    for (const message of batch.messages) {
      await processMessage(message.body);
    }
  }
}

Resource limits

workerd enforces resource limits through the IsolateLimitEnforcer interface:
  • CPU time limits
  • Memory limits
  • Request/response size limits
  • Subrequest limits
These limits ensure fair resource sharing and prevent runaway workers from affecting others.

Performance considerations

Minimize global scope work

Global scope code runs once per worker load:
// Bad: Heavy computation in global scope
const data = JSON.parse(largeJsonString);

// Good: Lazy initialization
let data = null;

export default {
  async fetch(request) {
    if (!data) {
      data = JSON.parse(largeJsonString);
    }
    return new Response('OK');
  }
}

Reuse connections

Create reusable objects in global scope:
const encoder = new TextEncoder();

export default {
  async fetch(request) {
    const bytes = encoder.encode('Hello');
    return new Response(bytes);
  }
}

Use streaming

Stream responses when possible:
export default {
  async fetch(request, env) {
    const response = await env.ORIGIN.fetch(request);
    return response; // Streams through without buffering
  }
}

Build docs developers (and LLMs) love