Workers runtime APIs
The Workers runtime is designed around web standards — the same APIs that ship in browsers (fetch, Request, Response, URL, URLPattern, Streams, WebCrypto, TextEncoder/Decoder) plus a curated set of platform extensions (HTMLRewriter, WebSocket server, Email handler, raw TCP sockets, Hyperdrive). For code that requires Node.js APIs, an opt-in nodejs_compat compatibility flag enables a subset of Node’s built-ins. The philosophical bet is that web-standards-first means the same code runs anywhere — Workers, Deno Deploy, Bun, browser service workers — without runtime-specific glue.
See also
- workers-runtime-and-deployment — how to enable compat flags
- workers-bindings-and-storage — bindings provide access to Cloudflare data products
- durable-objects-and-sqlite-storage — DOs use a superset of these APIs
1. fetch — the foundation API
Every Worker is built around fetch. Both incoming requests (your handler signature) and outgoing requests (your code calls fetch to hit external APIs) use the same Request / Response objects.
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Outgoing fetch — fully cached by Cloudflare's CDN automatically
const upstream = await fetch("https://api.example.com/data");
const data = await upstream.json();
return Response.json({ result: data });
},
};Differences from browser fetch:
- No CORS restrictions — Workers are server-side.
- Built-in caching —
fetchresults can be cached automatically viacf: { cacheTtl: 600 }:const resp = await fetch(url, { cf: { cacheTtl: 600, cacheEverything: true } }); - Subrequest limit — Free 50, Paid 1000 per invocation.
2. Request / Response / Headers / URL
Standard Web APIs:
const url = new URL(request.url);
const params = url.searchParams;
const headers = new Headers(request.headers);
headers.set("X-Custom", "value");
const body = await request.json(); // or .text(), .arrayBuffer(), .formData(), .blob()
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: {
"Content-Type": "application/json",
"Cache-Control": "max-age=60",
},
});
// Or shorthand:
return Response.json({ ok: true });
return Response.redirect("https://example.com/new", 301);3. URLPattern — for routing
const userPattern = new URLPattern({ pathname: "/users/:id" });
export default {
async fetch(request: Request): Promise<Response> {
const match = userPattern.exec(request.url);
if (match) {
const userId = match.pathname.groups.id;
return Response.json({ userId });
}
return new Response("Not found", { status: 404 });
},
};For larger apps, use a framework — Hono is the Cloudflare-native choice:
import { Hono } from "hono";
const app = new Hono();
app.get("/users/:id", (c) => c.json({ id: c.req.param("id") }));
app.post("/users", async (c) => c.json(await c.req.json()));
export default app;4. Streams — ReadableStream / WritableStream / TransformStream
Per developers.cloudflare.com/workers/runtime-apis/streams/. Standard Web Streams API. Used heavily for streaming responses:
export default {
async fetch(request: Request): Promise<Response> {
const { readable, writable } = new TransformStream();
// Background work: write to the stream
(async () => {
const writer = writable.getWriter();
const encoder = new TextEncoder();
for (let i = 0; i < 10; i++) {
await writer.write(encoder.encode(`Event ${i}\n`));
await new Promise(r => setTimeout(r, 1000));
}
await writer.close();
})();
return new Response(readable, {
headers: { "Content-Type": "text/plain" },
});
},
};Server-Sent Events pattern:
function sseResponse(): Response {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
const event = `data: ${JSON.stringify({ i })}\n\n`;
controller.enqueue(encoder.encode(event));
await new Promise(r => setTimeout(r, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
}5. WebCrypto
// SHA-256 hash
const data = new TextEncoder().encode("hello");
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashHex = Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
// HMAC
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode("secret"),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode("message"));
// UUID
const id = crypto.randomUUID();
// Random bytes
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);The full WebCrypto API is supported — AES, RSA, ECDSA, ECDH, PBKDF2, HKDF.
6. TextEncoder / TextDecoder
const encoder = new TextEncoder();
const bytes = encoder.encode("hello");
const decoder = new TextDecoder();
const str = decoder.decode(bytes);7. WebSocket server
Per developers.cloudflare.com/workers/runtime-apis/websockets/. Workers can accept WebSocket upgrades:
export default {
async fetch(request: Request): Promise<Response> {
if (request.headers.get("Upgrade") !== "websocket") {
return new Response("Expected websocket", { status: 426 });
}
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
server.accept();
server.addEventListener("message", (event) => {
server.send(`Echo: ${event.data}`);
});
server.addEventListener("close", (event) => {
console.log("Closed:", event.code, event.reason);
});
return new Response(null, { status: 101, webSocket: client });
},
};For production use cases (multiplayer games, chat rooms): use Durable Objects with WebSocket Hibernation — see durable-objects-and-sqlite-storage. Workers themselves are stateless and don’t persist connections after the handler returns.
8. HTMLRewriter
Cloudflare-specific. Streams HTML and lets you rewrite elements as they flow through:
export default {
async fetch(request: Request): Promise<Response> {
const response = await fetch(request);
return new HTMLRewriter()
.on("a", {
element(el) {
const href = el.getAttribute("href");
if (href?.startsWith("http://")) {
el.setAttribute("href", href.replace("http://", "https://"));
}
},
})
.on("script[src]", {
element(el) {
// Remove third-party scripts
if (!el.getAttribute("src")?.includes("example.com")) {
el.remove();
}
},
})
.transform(response);
},
};Uses a SAX-style streaming parser — runs in O(1) memory regardless of HTML size. Used heavily for reverse proxies, A/B testing, internationalization.
9. TCP sockets (connect)
Per developers.cloudflare.com/workers/runtime-apis/tcp-sockets/. Raw outbound TCP connections — useful for Postgres, Redis, MySQL, custom binary protocols:
import { connect } from "cloudflare:sockets";
export default {
async fetch(request: Request): Promise<Response> {
const socket = connect({
hostname: "redis.example.com",
port: 6379,
});
const writer = socket.writable.getWriter();
const reader = socket.readable.getReader();
await writer.write(new TextEncoder().encode("*1\r\n$4\r\nPING\r\n"));
const { value } = await reader.read();
console.log(new TextDecoder().decode(value)); // "+PONG\r\n"
await socket.close();
return new Response("Done");
},
};For Postgres / MySQL, use Hyperdrive (below) instead of raw TCP — it pools connections and caches reads.
10. Hyperdrive — managed Postgres / MySQL pooling
Per developers.cloudflare.com/hyperdrive/. Hyperdrive is a connection pool + query cache for Postgres and MySQL hosted on Cloudflare’s edge. Reasons:
- Cold starts on Workers + DB connections — opening a Postgres TCP connection from each Worker isolate is expensive (TLS handshake + auth). Hyperdrive maintains a warm pool.
- Connection limits — most Postgres instances cap at ~100-200 concurrent connections; Workers can spawn thousands. Hyperdrive multiplexes.
- Query caching — read queries can be cached at the edge (configurable per query or globally).
Setup:
wrangler hyperdrive create my-hyperdrive --connection-string "postgresql://user:pass@host:5432/db"[[hyperdrive]]
binding = "DB"
id = "abc-123"Use:
import { Client } from "pg"; // node-postgres works (with nodejs_compat)
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const client = new Client({
connectionString: env.DB.connectionString,
});
await client.connect();
const result = await client.query("SELECT * FROM users WHERE id = $1", [123]);
await client.end();
return Response.json(result.rows);
},
};Free: 100k queries/day. Paid: usage-based.
11. Email Workers
Per developers.cloudflare.com/email-routing/. Process inbound email at the edge:
export default {
async email(message: EmailMessage, env: Env, ctx: ExecutionContext) {
const from = message.from;
const to = message.to;
const headers = message.headers;
// Parse body
const reader = message.raw.getReader();
let body = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
body += new TextDecoder().decode(value);
}
if (from.includes("spam")) {
await message.setReject("Sorry, not accepting mail from this sender");
return;
}
// Forward to another address
await message.forward("alerts@example.com");
},
};Routing rules configured in the Cloudflare dashboard map incoming addresses to Worker handlers.
12. Node.js compatibility — nodejs_compat flag
Per developers.cloudflare.com/workers/runtime-apis/nodejs/. Enable in wrangler.toml:
compatibility_flags = ["nodejs_compat"]Adds support for a subset of Node.js built-ins:
| Module | Status |
|---|---|
node:assert | Yes |
node:async_hooks | Yes (AsyncLocalStorage) |
node:buffer | Yes (Buffer class) |
node:crypto | Yes (Node-style crypto on top of WebCrypto) |
node:dns | Yes |
node:events | Yes (EventEmitter) |
node:net | Yes (Socket — limited) |
node:path | Yes |
node:process | Yes (process.env, process.versions, etc.) |
node:stream | Yes |
node:string_decoder | Yes |
node:tls | Yes |
node:url | Yes |
node:util | Yes |
node:zlib | Yes |
node:fs | No (no filesystem) |
node:child_process | No (no process spawning) |
node:cluster / node:worker_threads | No |
node:http / node:https | Limited (server creation not supported) |
Most pure-JS npm packages work with nodejs_compat. Packages that need filesystem or process spawning don’t.
To enable Node-style globals (Buffer, process) without the node: prefix:
compatibility_flags = ["nodejs_compat", "nodejs_compat_populate_process_env"]13. Cache API
Per developers.cloudflare.com/workers/runtime-apis/cache/. The Worker has access to a per-POP cache (shared across all Workers in that POP for the same zone):
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const cacheKey = new Request(request.url, request);
const cache = caches.default;
let response = await cache.match(cacheKey);
if (!response) {
response = await fetchFromOrigin(request);
ctx.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
},
};Cache is per POP — a hit at one POP doesn’t help another. For global caching, use KV (slower writes, eventually consistent).
14. Workers AI binding (covered separately)
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await env.AI.run("@cf/meta/llama-3.3-70b-instruct", {
messages: [{ role: "user", content: "Hello!" }],
});
return Response.json(response);
},
};Full details in workers-ai-and-agents.
15. RPC between Workers (Service Bindings)
Per developers.cloudflare.com/workers/runtime-apis/rpc/. One Worker can call another Worker as a function (no HTTP overhead):
[[services]]
binding = "AUTH"
service = "auth-worker"// Service Worker (auth-worker)
import { WorkerEntrypoint } from "cloudflare:workers";
export default class AuthService extends WorkerEntrypoint {
async verifyToken(token: string): Promise<{ userId: string } | null> {
// ... verification logic
return { userId: "u_123" };
}
}
// Caller
const result = await env.AUTH.verifyToken("abc123");Zero-cost (no per-request billing, no network) compared to HTTP fetch between Workers. See more in hidden-tricks-and-gotchas-cloudflare-workers.
16. Misc — setTimeout, setInterval
Workers support setTimeout and setInterval, but they’re bounded:
- Timers cease when the invocation ends (unless wrapped in
ctx.waitUntil). - Max wall-clock for Free is 30s including I/O wait; Paid is 15 min with
waitUntil. - For real scheduled work, use Cron Triggers or Durable Object alarms.
17. Globals
The Worker global scope (globalThis) contains:
fetch,Request,Response,HeadersURL,URLPattern,URLSearchParamsTextEncoder,TextDecodercrypto(WebCrypto)console(logs flow towrangler tail)caches(per-POP Cache API)WebSocketPairBlob,File,FormDataReadableStream,WritableStream,TransformStreamsetTimeout,setInterval,clearTimeout,clearIntervalatob,btoastructuredCloneAbortController,AbortSignalEventTarget,Event,CustomEventWeakRef,FinalizationRegistry
Notably missing without nodejs_compat:
process,Buffer,global(Node-style)__dirname,__filenamerequire(always — Workers are ESM-only)
18. Limits to be aware of when designing
- Memory: 128 MB — applies to the isolate. Holding large in-memory data structures is bounded.
- CPU time — 10ms (Free), 30s default Paid, up to 5 min on Paid with config. CPU is active execution, not wall-clock.
- Subrequest limit — 50 (Free) / 1000 (Paid) per invocation. Each
fetchcounts; sometimes eachenv.KV.get()counts. - Script size — 3 MB (Free) / 10 MB (Paid) compressed. Large bundles fail to deploy.