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:
- Isolate allocation: A V8 isolate is created (or reused if compatible)
- API setup: JavaScript APIs are registered
- 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:
- Worker instance selected: An available worker instance is chosen
- IoContext created: Per-request context is established
- Handler invoked: The
fetch() handler executes
- Response returned: The Response is serialized and sent
- 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.
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
}
}