*Last updated: May 2025 — based on official Cloudflare Workers documentation.*
Resources
10Install
npx skillscat add hexamh/exchangex Install via the SkillsCat registry.
Cloudflare Workers Runtime APIs & Best Practices
Comprehensive reference for the Workers runtime — covering every API surface, TypeScript patterns, Node.js compatibility, and production best practices. Sourced from official Cloudflare documentation (May 2025).
Table of Contents
- Runtime Model
- Worker Entry Points & Handlers
- Request API
- Response API
- Fetch API
- Headers API
- Context (ctx) API
- Cache API
- Streams API
- HTMLRewriter API
- WebSockets API
- Web Crypto API
- Encoding API
- Performance & Timers
- Scheduler API
- TCP Sockets
- EventSource (SSE)
- MessageChannel & MessagePort
- Console API
- Web Standards Available
- WebAssembly (Wasm)
- Remote Procedure Call (RPC)
- Bindings (env)
- Node.js Compatibility
- TypeScript
- Best Practices
- Platform Limits Reference
- Wrangler Configuration Reference
1. Runtime Model
The Workers runtime runs on V8 isolates — not containers or Node.js processes.
Key properties
- Isolate-based: Each Worker is a V8 isolate. A single machine hosts thousands of isolates with near-zero startup overhead (~100× faster than a container cold start).
- Single-threaded event loop: One isolate handles requests sequentially in an async event loop. Concurrent requests may interleave at
awaitpoints. - No persistent global state: Do not store mutable state in global scope — isolates may be evicted at any time. Use KV, D1, R2, or Durable Objects for persistence.
- Standards-first: APIs follow WinterCG / web-interoperable standards (Fetch, Streams, Web Crypto, URL, etc.).
- Compatibility dates: Control which API behaviors and bug fixes apply to your Worker. Always set
compatibility_dateto today on new projects.
// wrangler.jsonc
{
"compatibility_date": "2025-05-01",
"compatibility_flags": ["nodejs_compat"]
}2. Worker Entry Points & Handlers
Workers use ES Module syntax (preferred over legacy Service Worker syntax).
Default export (stateless Worker)
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response("Hello World");
},
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
// Cron trigger handler
},
async tail(events: TraceItem[], env: Env, ctx: ExecutionContext): Promise<void> {
// Tail Worker — receives log/trace data from other Workers
},
} satisfies ExportedHandler<Env>;Named entrypoint (WorkerEntrypoint — for RPC / Service Bindings)
import { WorkerEntrypoint } from "cloudflare:workers";
export class AuthService extends WorkerEntrypoint<Env> {
async checkToken(token: string): Promise<{ valid: boolean; userId?: string }> {
const userId = await this.env.KV.get(`token:${token}`);
return userId ? { valid: true, userId } : { valid: false };
}
}Handler types
| Handler | Trigger | Signature |
|---|---|---|
fetch |
HTTP request | (request, env, ctx) => Response | Promise<Response> |
scheduled |
Cron trigger | (event, env, ctx) => void | Promise<void> |
tail |
Tail Worker log stream | (events, env, ctx) => void | Promise<void> |
email |
Email Routing | (message, env, ctx) => void | Promise<void> |
queue |
Queue consumer | (batch, env, ctx) => void | Promise<void> |
3. Request API
The Request object follows the Fetch API standard with Cloudflare-specific extensions.
Constructor
new Request(input: string | URL | Request, init?: RequestInit)Properties
| Property | Type | Description |
|---|---|---|
url |
string |
Full URL of the request |
method |
string |
HTTP method (GET, POST, etc.) |
headers |
Headers |
Request headers |
body |
ReadableStream | null |
Request body as a stream |
bodyUsed |
boolean |
Whether body has been consumed |
redirect |
string |
"follow" | "error" | "manual" |
cf |
IncomingRequestCfProperties |
Cloudflare-specific metadata (see below) |
Body consumption methods
await request.text() // → string
await request.json() // → unknown
await request.arrayBuffer() // → ArrayBuffer
await request.formData() // → FormData
await request.blob() // → Blob
request.body // → ReadableStream | null
request.clone() // → Request (clone before consuming body twice)Cloudflare cf properties (IncomingRequestCfProperties)
request.cf.country // ISO country code, e.g. "US"
request.cf.continent // e.g. "NA"
request.cf.city // e.g. "Austin"
request.cf.region // e.g. "Texas"
request.cf.regionCode // e.g. "TX"
request.cf.latitude // e.g. "30.2672"
request.cf.longitude // e.g. "-97.7431"
request.cf.postalCode // e.g. "78701"
request.cf.timezone // e.g. "America/Chicago"
request.cf.asn // Autonomous System Number
request.cf.asOrganization // ASN org name
request.cf.colo // Cloudflare data center IATA code, e.g. "DFW"
request.cf.tlsVersion // e.g. "TLSv1.3"
request.cf.tlsCipher // e.g. "AEAD-AES128-GCM-SHA256"
request.cf.httpProtocol // e.g. "HTTP/2"
request.cf.botManagement // Bot score, verified bot, etc.
request.cf.hostMetadata // Custom metadata (Workers for Platforms)Custom fetch options via cf
const response = await fetch(url, {
cf: {
cacheEverything: true,
cacheTtl: 3600,
cacheTtlByStatus: { "200-299": 86400, "404": 10, "500-599": 0 },
resolveOverride: "alternate-origin.example.com",
scrapeShield: false,
minify: { javascript: true, css: true, html: true },
},
});4. Response API
new Response(body?: BodyInit | null, init?: ResponseInit)Properties
| Property | Type | Description |
|---|---|---|
status |
number |
HTTP status code |
statusText |
string |
Status message |
ok |
boolean |
true if status 200–299 |
headers |
Headers |
Response headers |
body |
ReadableStream | null |
Body stream |
bodyUsed |
boolean |
Whether body was consumed |
url |
string |
Final URL after redirects |
redirected |
boolean |
Whether redirect occurred |
webSocket |
WebSocket | null |
Present on 101 upgrade responses |
Static helpers
// JSON response
Response.json({ key: "value" }, { status: 200 })
// Redirect
Response.redirect("https://example.com", 302)
// Clone
response.clone()Workers-specific ResponseInit options
new Response(body, {
status: 200,
statusText: "OK",
headers: { "Content-Type": "application/json" },
// Workers-specific:
encodeBody: "automatic" | "manual", // control compression
webSocket: ws, // for 101 WebSocket upgrades
cf: { /* informational metadata */ },
})Body methods
await response.text()
await response.json()
await response.arrayBuffer()
await response.formData()
await response.blob()5. Fetch API
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>- Only available inside the request context (inside handler callbacks).
- Supports HTTP/1.1 and HTTP/2 outbound.
- Returns a streaming
Response— do not buffer unless needed. - Maximum 50 subrequests per request (free) / 1000 (paid).
// Basic fetch
const res = await fetch("https://api.example.com/data");
const data = await res.json();
// Fetch with options
const res = await fetch("https://api.example.com/post", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hello: "world" }),
});
// Pass-through request to origin
return fetch(request);
// Modify and pass through
const modified = new Request(request, {
headers: { ...Object.fromEntries(request.headers), "X-Custom": "value" },
});
return fetch(modified);6. Headers API
new Headers(init?: HeadersInit)const headers = new Headers();
headers.set("Content-Type", "application/json");
headers.append("Set-Cookie", "a=1");
headers.append("Set-Cookie", "b=2");
headers.get("content-type"); // case-insensitive
headers.has("x-custom");
headers.delete("x-custom");
headers.getAll("set-cookie"); // Cloudflare extension: returns string[]
// Iterate
for (const [name, value] of headers) { }
headers.forEach((value, name) => { });
[...headers.entries()]
[...headers.keys()]
[...headers.values()]Workers extension: getAll()
The getAll(name) method (non-standard) returns all values for a header as an array — essential for Set-Cookie which legitimately has multiple values.
7. Context (ctx) API
ctx is the ExecutionContext passed to every handler.
ctx.waitUntil(promise: Promise<any>): void
Extends Worker lifetime past the response. Use for:
- Firing analytics events
- Writing to cache
- Background mutations
export default {
async fetch(request, env, ctx) {
const response = new Response("OK");
// Fire and forget — does not block response
ctx.waitUntil(
env.ANALYTICS.writeDataPoint({ blobs: [request.url], doubles: [1] })
);
return response;
},
};Worker lifetime extended up to 30 seconds after response is sent.
ctx.passThroughOnException(): void
Fail-open: if the Worker throws an unhandled exception, the request is forwarded to the origin instead of returning a 500 error.
export default {
async fetch(request, env, ctx) {
ctx.passThroughOnException();
// If anything below throws, request falls through to origin
return await riskyOperation(request, env);
},
};⚠️ Cannot recover streaming bodies — clone the body before consuming if you need fail-open with body passthrough.
ctx.props (WorkerEntrypoint only)
Props passed from a calling Worker via Service Bindings.
export class MyService extends WorkerEntrypoint<Env, { userId: string }> {
async greet() {
return `Hello, ${this.ctx.props.userId}`;
}
}ctx.exports (Dynamic Worker Loader)
Expose RPC stubs from within a named entrypoint context.
8. Cache API
The Cache API lets Workers read from and write to Cloudflare's global CDN cache. It is only available in the request context.
// Access the default cache
const cache = caches.default;
// Access a named cache
const myCache = await caches.open("my-cache-v1");Methods
// Store a response in cache
await cache.put(request: Request | string, response: Response): Promise<void>
// Retrieve from cache
const cached = await cache.match(request: Request | string, options?: { ignoreMethod?: boolean }): Promise<Response | undefined>
// Invalidate
await cache.delete(request: Request | string, options?: { ignoreMethod?: boolean }): Promise<boolean>Caching pattern
export default {
async fetch(request, env, ctx) {
const cache = caches.default;
const cacheKey = new Request(request.url, { method: "GET" });
let response = await cache.match(cacheKey);
if (response) return response;
response = await fetch(request);
// Only cache successful responses
if (response.ok) {
const toCache = new Response(response.body, response);
toCache.headers.set("Cache-Control", "public, max-age=3600");
ctx.waitUntil(cache.put(cacheKey, toCache));
}
return response;
},
};Cache rules
- Respects standard
Cache-Controlheaders. - Use
cf.cacheTtl/cf.cacheTtlByStatuson outboundfetch()for fine control. - Named caches are scoped to the Worker script — not shared globally.
- Cache
PUTrequires the response URL to match the request URL (same origin only).
9. Streams API
Workers implement the WHATWG Streams standard. All request/response bodies are streams — the runtime does not buffer automatically.
ReadableStream
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("Hello "));
controller.enqueue(new TextEncoder().encode("World"));
controller.close();
},
});
// Pipe to response
return new Response(stream);TransformStream
const { readable, writable } = new TransformStream({
transform(chunk, controller) {
// Modify chunk
controller.enqueue(chunk);
},
flush(controller) {
controller.terminate();
},
});
// Pipe: source → transform → response
sourceStream.pipeTo(writable);
return new Response(readable);WritableStream
const writable = new WritableStream({
write(chunk) { /* handle chunk */ },
close() { /* done */ },
abort(reason) { /* error */ },
});Pipe operations
readable.pipeTo(writable) // → Promise<void>
readable.pipeThrough(transformStream) // → ReadableStream
readable.tee() // → [ReadableStream, ReadableStream]ReadableStream readers
// Default reader (object mode or text)
const reader = stream.getReader();
const { done, value } = await reader.read(); // value is Uint8Array
// BYOB reader (binary, zero-copy)
const byobReader = stream.getReader({ mode: "byob" });
const buffer = new Uint8Array(1024);
const { done, value } = await byobReader.read(buffer);10. HTMLRewriter API
HTMLRewriter is a streaming HTML parser and transformer. It processes HTML in a single pass with CSS-selector-based handlers — no DOM tree is built.
const rewriter = new HTMLRewriter()
.on("a[href]", new LinkRewriter())
.on("title", new TitleHandler())
.on("div.hero", new HeroHandler())
.onDocument(new DocHandler());
return rewriter.transform(response);Element handler
class LinkRewriter {
element(element: Element) {
const href = element.getAttribute("href");
if (href?.startsWith("/")) {
element.setAttribute("href", `https://cdn.example.com${href}`);
}
}
comments(comment: Comment) { }
text(text: Text) { }
}Element methods
element.tagName // "div", "a", etc.
element.getAttribute(name) // string | null
element.setAttribute(name, value)
element.removeAttribute(name)
element.hasAttribute(name)
element.attributes // Iterator<[string, string]>
element.before(content, { html: true }) // insert before opening tag
element.after(content, { html: true }) // insert after closing tag
element.prepend(content, { html: true }) // insert after opening tag
element.append(content, { html: true }) // insert before closing tag
element.replace(content, { html: true })
element.remove()
element.removeAndKeepContent()
element.setInnerContent(content, { html: true })
element.onEndTag((endTag) => { }) // called on </tag>Document handler
class DocHandler {
doctype(doctype: Doctype) { }
comments(comment: Comment) { }
text(text: Text) { }
end(end: DocumentEnd) {
end.append("<script>console.log('injected')</script>", { html: true });
}
}Async handlers
Handlers can be async — they can await external resources (KV lookups, fetch calls) during streaming.
class AsyncHandler {
async element(element: Element) {
const data = await env.KV.get("some-key");
element.setAttribute("data-value", data ?? "");
}
}11. WebSockets API
Workers support WebSockets both as a server (accepting upgrade requests) and as a client (connecting to external WS servers).
Server-side (Worker as WS server)
export default {
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get("Upgrade");
if (upgradeHeader !== "websocket") {
return new Response("Expected WebSocket", { status: 426 });
}
const { 0: client, 1: server } = new WebSocketPair();
server.accept();
server.addEventListener("message", (event) => {
server.send(`Echo: ${event.data}`);
});
server.addEventListener("close", () => { });
return new Response(null, { status: 101, webSocket: client });
},
};Client-side (Worker as WS client)
const response = await fetch("wss://external.example.com/ws", {
headers: { Upgrade: "websocket" },
});
const ws = response.webSocket!;
ws.accept();
ws.send("hello");
ws.addEventListener("message", (event) => {
console.log(event.data);
});WebSocket Hibernation API (Durable Objects)
The Hibernation API lets a Durable Object sleep between messages, dramatically reducing billing.
import { DurableObject } from "cloudflare:workers";
export class ChatRoom extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
const { 0: client, 1: server } = new WebSocketPair();
// Use ctx.acceptWebSocket instead of server.accept()
// Tags allow retrieval of specific WebSockets later
this.ctx.acceptWebSocket(server, ["user:123"]);
return new Response(null, { status: 101, webSocket: client });
}
// Called by runtime on each message (DO wakes from hibernation)
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
const sockets = this.ctx.getWebSockets(); // all connected
const tagged = this.ctx.getWebSockets("user:123"); // by tag
for (const sock of sockets) {
sock.send(message);
}
}
async webSocketClose(ws: WebSocket, code: number, reason: string): Promise<void> { }
async webSocketError(ws: WebSocket, error: unknown): Promise<void> { }
}WebSocket message size
Maximum WebSocket message size: 32 MiB (increased from 1 MiB in 2024).
WebSocket binary type (compat flag)
With websocket_standard_binary_type flag (auto-enabled for compat date ≥ 2026-03-17), binaryType defaults to "blob". Otherwise defaults to "arraybuffer".
12. Web Crypto API
Full implementation of the Web Crypto API.
// Secure random values
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
const uuid = crypto.randomUUID();
// SubtleCrypto
const subtle = crypto.subtle;Common operations
// Generate a key
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // extractable
["encrypt", "decrypt"]
);
// Encrypt
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
new TextEncoder().encode("secret data")
);
// Decrypt
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
encrypted
);
// HMAC sign
const hmacKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode("my-secret"),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
const signature = await crypto.subtle.sign("HMAC", hmacKey, data);
// Timing-safe comparison (CRITICAL for secret comparison)
const isEqual = await crypto.subtle.timingSafeEqual(bufA, bufB);
// Hash
const hash = await crypto.subtle.digest("SHA-256", data);
// PBKDF2 key derivation
const baseKey = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveKey"]);
const derivedKey = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" },
baseKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);Key import/export formats
| Format | Use |
|---|---|
"raw" |
Raw bytes (symmetric keys) |
"pkcs8" |
Private keys (RSA, EC) |
"spki" |
Public keys |
"jwk" |
JSON Web Key |
Supported algorithms
- Encryption: AES-CBC, AES-CTR, AES-GCM, RSA-OAEP
- Signing: HMAC, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA
- Key Agreement: ECDH, X25519
- Hashing: SHA-1, SHA-256, SHA-384, SHA-512
- Key Derivation: PBKDF2, HKDF
- Key Generation: AES, RSA, EC (P-256, P-384, P-521), Ed25519, X25519
⚠️ Never use
Math.random()for security-sensitive values. Always usecrypto.getRandomValues().
13. Encoding API
// TextEncoder — UTF-8 only
const encoder = new TextEncoder();
const bytes = encoder.encode("Hello, World!"); // → Uint8Array
encoder.encoding; // "utf-8"
// TextDecoder — UTF-8 (and others with nodejs_compat)
const decoder = new TextDecoder("utf-8");
const text = decoder.decode(bytes); // → string
decoder.decode(bytes, { stream: true }); // streaming decode
// atob / btoa — Base64
const b64 = btoa("Hello"); // "SGVsbG8="
const str = atob("SGVsbG8="); // "Hello"
// Uint8Array base64 (new V8 native — 2024+)
const encoded = Uint8Array.fromBase64("SGVsbG8=");
const decoded = uint8Array.toBase64();
const hex = uint8Array.toHex();
const fromHex = Uint8Array.fromHex("48656c6c6f");14. Performance & Timers
performance.now()
Returns time elapsed since last I/O event — does not advance during CPU-only code execution (intentional for security). Useful for measuring I/O latency.
const start = performance.now();
const result = await env.KV.get("key");
const elapsed = performance.now() - start; // ms elapsed during KV fetchperformance.timeOrigin
Always 0 in Workers. performance.now() effectively equals Date.now().
Timers
setTimeout(fn, delay) // Available, but not recommended for production logic
setInterval(fn, delay) // Available
clearTimeout(id)
clearInterval(id)⚠️ Timers fire only within the current request context. They do not persist across requests. Use Durable Object Alarms or Cron Triggers for reliable scheduling.
Date
Date.now() // current Unix timestamp in ms
new Date() // current Date object15. Scheduler API
scheduler.wait() pauses execution for a given number of milliseconds. Useful in test environments; in production, the Worker must still complete within CPU time limits.
await scheduler.wait(1000); // wait 1 second16. TCP Sockets
Workers can open raw TCP connections using connect().
import { connect } from "cloudflare:sockets";
const socket = connect({ hostname: "db.example.com", port: 5432 });
const writer = socket.writable.getWriter();
const reader = socket.readable.getReader();
await writer.write(new TextEncoder().encode("PING\r\n"));
const { value } = await reader.read();
// TLS upgrade
const tlsSocket = socket.startTls({ expectedServerHostname: "db.example.com" });
// Close
await socket.close();Socket properties
socket.readable // ReadableStream<Uint8Array>
socket.writable // WritableStream<Uint8Array>
socket.closed // Promise<void> — resolves when closed
socket.opened // Promise<SocketInfo> — resolves when connectedUse case: Raw database connections
TCP sockets are used by Hyperdrive and direct database drivers (PostgreSQL via postgres npm package, etc.) to connect to external databases.
17. EventSource (SSE)
Workers can consume Server-Sent Events from upstream servers.
const eventSource = new EventSource("https://api.example.com/events");
eventSource.addEventListener("message", (event) => {
console.log(event.data);
});
eventSource.addEventListener("custom-event", (event) => {
console.log(event.data);
});
eventSource.onerror = (error) => {
console.error("EventSource error", error);
};
eventSource.close();Note: Workers can both consume SSE (via
EventSource) and produce SSE (by returning a streamingResponsewithContent-Type: text/event-stream).
Producing SSE from a Worker
export default {
async fetch(request: Request): Promise<Response> {
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
// Stream events in background
const send = async () => {
for (let i = 0; i < 10; i++) {
await writer.write(encoder.encode(`data: event ${i}\n\n`));
await scheduler.wait(1000);
}
writer.close();
};
send(); // don't await — non-blocking
return new Response(readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
},
};18. MessageChannel & MessagePort
Standard web MessageChannel and MessagePort APIs are available in Workers (added in 2024).
const { port1, port2 } = new MessageChannel();
port1.onmessage = (event) => {
console.log("port1 received:", event.data);
};
port2.postMessage({ hello: "world" });
port2.close();Used for structured communication between components within the same isolate.
19. Console API
console.log("message")
console.error("error")
console.warn("warning")
console.info("info")
console.debug("debug")
console.dir(object)
console.table(array)
console.time("label")
console.timeEnd("label")
console.count("label")
console.countReset("label")
console.group("label")
console.groupEnd()
console.trace()
console.assert(condition, "message")Output appears in:
wrangler devterminal- Cloudflare Dashboard real-time logs
- Tail Workers (via
TailItem.logs)
20. Web Standards Available
Workers implement the following standard browser APIs:
URL & URLSearchParams
const url = new URL(request.url);
url.hostname // "api.example.com"
url.pathname // "/users/123"
url.searchParams.get("page") // "1"
url.searchParams.set("limit", "50")
const params = new URLSearchParams("a=1&b=2");
params.getAll("a") // ["1"]URLPattern
const pattern = new URLPattern({ pathname: "/users/:id" });
const match = pattern.exec("https://example.com/users/42");
match?.pathname.groups.id // "42"AbortController & AbortSignal
const controller = new AbortController();
const { signal } = controller;
setTimeout(() => controller.abort(), 5000); // abort after 5s
const response = await fetch(url, { signal });FormData
const formData = await request.formData();
formData.get("field");
formData.getAll("files");
formData.set("key", "value");Blob
const blob = new Blob(["hello"], { type: "text/plain" });
await blob.text();
await blob.arrayBuffer();
blob.size;
blob.type;File
const file = new File(["content"], "file.txt", { type: "text/plain" });
file.name;
file.lastModified;Structured Clone
const cloned = structuredClone({ a: 1, date: new Date() });Intl
const formatter = new Intl.DateTimeFormat("en-US", { timeZone: "America/New_York" });
formatter.format(new Date());Navigator (with flag)
When global_navigator compatibility flag is set:
navigator.userAgent // "Cloudflare-Workers"
navigator.sendBeacon(url, data)Promise events
addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
});
addEventListener("rejectionhandled", (event) => { });Web File System API (with flag)
When enable_web_file_system flag is set, the File System Access API provides access to the same in-memory virtual filesystem as node:fs.
21. WebAssembly (Wasm)
Workers can execute WebAssembly modules.
Import Wasm in module Worker
// wrangler.jsonc — declare the binding
// [[rules]]
// type = "CompiledWasm"
// globs = ["**/*.wasm"]
import wasm from "./module.wasm";
const instance = await WebAssembly.instantiate(wasm, { /* imports */ });
instance.exports.myFunction(42);Wasm global APIs
WebAssembly.compile(bufferSource) // → Promise<WebAssembly.Module>
WebAssembly.instantiate(module, importObject) // → Promise<WebAssembly.Instance>
WebAssembly.validate(bufferSource) // → boolean
new WebAssembly.Module(bufferSource)
new WebAssembly.Instance(module, importObject)
new WebAssembly.Memory({ initial: 1, maximum: 10 })
new WebAssembly.Table({ element: "anyfunc", initial: 10 })22. Remote Procedure Call (RPC)
Workers RPC lets Workers call methods on other Workers or Durable Objects as if they were local functions — no HTTP serialization required.
WorkerEntrypoint (service binding target)
import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers";
export class MathService extends WorkerEntrypoint<Env> {
// Public methods are callable via RPC
async add(a: number, b: number): Promise<number> {
return a + b;
}
// Return an RpcTarget for capability-based access
async getUser(id: string): Promise<UserStub> {
const data = await this.env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(id).first();
return new UserStub(data);
}
}
class UserStub extends RpcTarget {
constructor(private data: Record<string, unknown>) { super(); }
getName(): string { return this.data.name as string; }
}Calling an RPC service (caller Worker)
// wrangler.jsonc — declare service binding with entrypoint
// [[services]]
// binding = "MATH"
// service = "math-worker"
// entrypoint = "MathService"
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const result = await env.MATH.add(2, 3); // → 5
const user = await env.MATH.getUser("123");
const name = await user.getName();
return Response.json({ result, name });
},
};RPC serialization (Structured Clone + extras)
- ✅ All Structured Cloneable types (Date, Map, Set, ArrayBuffer, etc.)
- ✅ Functions (become remote stubs — calling them makes another RPC)
- ✅
RpcTargetsubclasses (returned as stubs) - ✅
ReadableStream,WritableStream(piped across boundary) - ❌ DOM nodes, Symbols, non-cloneable objects
RPC lifecycle
// Stubs are disposed when the request ends
// Use explicit disposal for long-lived stubs:
using stub = await env.SERVICE.getStub(); // 'using' keyword (TC39)Durable Object RPC
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject<Env> {
async increment(): Promise<number> {
const value = (await this.ctx.storage.get<number>("count")) ?? 0;
const next = value + 1;
await this.ctx.storage.put("count", next);
return next;
}
}
// In the Worker:
const id = env.COUNTER.idFromName("global");
const stub = env.COUNTER.get(id);
const count = await stub.increment(); // RPC call23. Bindings (env)
Bindings connect Workers to Cloudflare services — they are in-process references with no network hop, no auth, and near-zero latency.
⚠️ Always use bindings over REST APIs when calling Cloudflare services from within a Worker.
KV Namespace
// env.KV is a KVNamespace
await env.KV.put("key", "value");
await env.KV.put("key", "value", { expirationTtl: 3600 });
await env.KV.put("key", JSON.stringify(obj), { metadata: { tag: "v1" } });
const value = await env.KV.get("key"); // string | null
const value = await env.KV.get("key", "arrayBuffer"); // ArrayBuffer | null
const value = await env.KV.get("key", "stream"); // ReadableStream | null
const value = await env.KV.get("key", { type: "json" }); // T | null
const { value, metadata } = await env.KV.getWithMetadata("key");
await env.KV.delete("key");
// List keys
const list = await env.KV.list({ prefix: "user:", limit: 100, cursor: undefined });
list.keys; // [{ name: string, expiration?: number, metadata?: unknown }]
list.list_complete;
list.cursor;D1 Database
// env.DB is a D1Database
const stmt = env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(userId);
const row = await stmt.first<User>();
const { results } = await stmt.all<User>();
const { results } = await env.DB.prepare("SELECT * FROM users").all<User>();
// Batch (atomic)
const results = await env.DB.batch([
env.DB.prepare("INSERT INTO users (id, name) VALUES (?, ?)").bind(id, name),
env.DB.prepare("UPDATE users SET updated_at = ? WHERE id = ?").bind(Date.now(), id),
]);
// Exec (multi-statement DDL)
await env.DB.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at INTEGER DEFAULT (unixepoch())
);
`);R2 Bucket
// env.BUCKET is an R2Bucket
await env.BUCKET.put("key", body, {
httpMetadata: { contentType: "image/png" },
customMetadata: { uploadedBy: "user-123" },
});
const object = await env.BUCKET.get("key");
if (object) {
const headers = new Headers();
object.writeHttpMetadata(headers);
return new Response(object.body, { headers });
}
await env.BUCKET.delete("key");
// List
const list = await env.BUCKET.list({ prefix: "images/", limit: 50, cursor });
list.objects; // R2Object[]
list.truncated;
list.cursor;
// Head (metadata only, no body)
const head = await env.BUCKET.head("key");
// Multipart upload
const upload = await env.BUCKET.createMultipartUpload("large-file.bin");
const part1 = await upload.uploadPart(1, chunk1);
const part2 = await upload.uploadPart(2, chunk2);
await upload.complete([part1, part2]);Durable Objects
// env.MY_DO is a DurableObjectNamespace
const id = env.MY_DO.idFromName("room-123"); // deterministic
const id = env.MY_DO.idFromString(hexId); // from stored ID
const id = env.MY_DO.newUniqueId(); // random
const stub = env.MY_DO.get(id);
const response = await stub.fetch(request); // HTTP
const result = await stub.myRpcMethod(arg); // RPC (requires DurableObject base class)
stub.id.toString(); // hex string to store/retrieve laterAI Binding (Workers AI)
// env.AI is an Ai binding
const result = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: "What is the capital of France?",
});
// Streaming
const stream = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: "Tell me a story",
stream: true,
});
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });Queues
// Producer
await env.MY_QUEUE.send({ type: "email", to: "user@example.com" });
await env.MY_QUEUE.sendBatch([{ body: msg1 }, { body: msg2 }]);
// Consumer (handler)
export default {
async queue(batch: MessageBatch, env: Env): Promise<void> {
for (const message of batch.messages) {
await processMessage(message.body);
message.ack(); // or message.retry()
}
// Or: batch.ackAll() / batch.retryAll()
},
};Vectorize
// env.INDEX is a VectorizeIndex
await env.INDEX.upsert([{ id: "doc-1", values: [0.1, 0.2, ...], metadata: { title: "..." } }]);
const results = await env.INDEX.query([0.1, 0.2, ...], {
topK: 5,
returnMetadata: "all",
filter: { category: "tech" },
});
results.matches; // [{ id, score, metadata }]
await env.INDEX.deleteByIds(["doc-1", "doc-2"]);
const vectors = await env.INDEX.getByIds(["doc-1"]);Service Bindings
// HTTP-style
const response = await env.OTHER_WORKER.fetch(request);
// RPC-style (preferred)
const result = await env.OTHER_WORKER.myMethod(arg);Rate Limiting Binding
const { success } = await env.RATE_LIMITER.limit({ key: request.headers.get("CF-Connecting-IP") ?? "anon" });
if (!success) return new Response("Too Many Requests", { status: 429 });Analytics Engine
env.ANALYTICS.writeDataPoint({
blobs: [request.cf?.country ?? "unknown", url.pathname],
doubles: [responseTime, statusCode],
indexes: [userId],
});
// Does NOT need ctx.waitUntil — non-blocking by designmTLS Binding
const response = await fetch("https://internal.example.com/api", {
// @ts-ignore — cf extension
cf: { mtlsClientCertificate: env.CLIENT_CERT },
});Hyperdrive
import { Client } from "pg";
const client = new Client({ connectionString: env.HYPERDRIVE.connectionString });
await client.connect();
const result = await client.query("SELECT * FROM users WHERE id = $1", [userId]);
await client.end();Version Metadata (beta)
env.CF_VERSION_METADATA.id // deployment version ID
env.CF_VERSION_METADATA.tag // optional version tag
env.CF_VERSION_METADATA.timestamp // ISO timestamp of deployment24. Node.js Compatibility
Enable with compatibility_flags: ["nodejs_compat"] and compatibility_date >= "2024-09-23".
Supported built-in modules (🟢 = full, 🟡 = partial)
| Module | Status | Notes |
|---|---|---|
node:assert |
🟢 | Full assert API |
node:async_hooks |
🟢 | AsyncLocalStorage, AsyncResource |
node:buffer |
🟢 | Buffer class, encoding helpers |
node:crypto |
🟢 | Hash, HMAC, cipher, key generation |
node:diagnostics_channel |
🟢 | Pub/sub channel diagnostics |
node:dns |
🟡 | dns.resolve* (no dns.lookup) |
node:events |
🟢 | EventEmitter |
node:fs |
🟢 | In-memory virtual FS (flag: enable_nodejs_fs_module) |
node:http |
🟡 | Partial — use fetch() for outbound HTTP |
node:https |
🟡 | Partial |
node:net |
🟡 | TCP — backed by connect() |
node:path |
🟢 | Path manipulation |
node:process |
🟢 | process.env, process.exit, etc. |
node:stream |
🟢 | Readable, Writable, Transform, pipeline |
node:string_decoder |
🟢 | StringDecoder |
node:test |
🟢 | Built-in test runner |
node:timers |
🟢 | setTimeout, setInterval, setImmediate |
node:tls |
🟡 | TLS sockets |
node:url |
🟢 | URL, URLSearchParams |
node:util |
🟢 | promisify, inspect, types |
node:zlib |
🟢 | Compression: gzip, deflate, brotli |
AsyncLocalStorage (most important for DI/tracing)
import { AsyncLocalStorage } from "node:async_hooks";
const requestContext = new AsyncLocalStorage<{ requestId: string }>();
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const ctx = { requestId: crypto.randomUUID() };
return requestContext.run(ctx, async () => {
return handleRequest(request, env);
});
},
};
function handleRequest(request: Request, env: Env): Promise<Response> {
const { requestId } = requestContext.getStore()!;
// requestId available anywhere in this async call tree
return fetch(request);
}Buffer
import { Buffer } from "node:buffer";
const buf = Buffer.from("hello", "utf8");
buf.toString("base64"); // "aGVsbG8="
buf.toString("hex"); // "68656c6c6f"
Buffer.alloc(16);
Buffer.concat([buf1, buf2]);node:crypto
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
const hash = createHash("sha256").update("data").digest("hex");
const hmac = createHmac("sha256", "secret").update("data").digest("base64");
const random = randomBytes(32); // Buffer
timingSafeEqual(bufA, bufB); // constant-time comparisonprocess.env (with flag)
With nodejs_compat_populate_process_env flag, text bindings are available on process.env:
process.env.MY_SECRET // populated from Workers text bindings25. TypeScript
Setup
npm create cloudflare@latest -- my-worker --type hello-world --tsGenerate types from Wrangler config (preferred)
npx wrangler typesThis generates worker-configuration.d.ts (or .wrangler/types/runtime.d.ts for older Wrangler) with:
- Correct
Envinterface matching your bindings - Runtime types based on your
compatibility_dateandcompatibility_flags DurableObjectNamespace<T>typed with your DO class
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ESNext"],
"strict": true,
"types": ["./worker-configuration.d.ts"]
}
}Run
wrangler typesbefore anytsc,vitest, or build step.
Typed Worker export
import type { ExportedHandler } from "cloudflare:workers";
export default {
async fetch(request, env, ctx): Promise<Response> {
return new Response("OK");
},
} satisfies ExportedHandler<Env>;Typed Durable Object
import { DurableObject } from "cloudflare:workers";
export class MyDO extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
return new Response("OK");
}
}RPC TypeScript
// wrangler types generates:
// interface Env { MATH_SERVICE: Service<MathService>; }
const sum = await env.MATH_SERVICE.add(1, 2); // → number (fully typed)tsconfig recommendations
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"moduleResolution": "Bundler",
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"jsx": "react-jsx"
}
}ESLint rules for Workers
// .eslintrc.json
{
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
"no-console": "warn"
}
}26. Best Practices
Compatibility & Configuration
// Always set compatibility_date to today on new projects
// Periodically bump on existing projects
{
"compatibility_date": "2025-05-01",
"compatibility_flags": ["nodejs_compat"]
}Never mutate global state
// ❌ WRONG — isolates can be evicted; global state is unreliable
let counter = 0;
export default {
async fetch() { counter++; return Response.json({ counter }); }
};
// ✅ CORRECT — use Durable Objects or KV for state
export default {
async fetch(request, env) {
const count = await env.DO_STUB.increment();
return Response.json({ count });
}
};Always await async operations
// ❌ WRONG — floating promise, request may end before it completes
export default {
async fetch(request, env) {
env.KV.put("key", "value"); // NOT awaited, NOT in waitUntil
return new Response("OK");
}
};
// ✅ CORRECT — use ctx.waitUntil for fire-and-forget
export default {
async fetch(request, env, ctx) {
ctx.waitUntil(env.KV.put("key", "value"));
return new Response("OK");
}
};Use bindings over REST APIs
// ❌ WRONG — REST API adds latency and requires auth
const res = await fetch(`https://api.cloudflare.com/client/v4/kv/...`, {
headers: { Authorization: `Bearer ${env.CF_TOKEN}` },
});
// ✅ CORRECT — binding is in-process, zero latency
const value = await env.KV.get("key");Timing-safe secret comparison
// ❌ WRONG — vulnerable to timing attacks
if (providedToken === env.SECRET_TOKEN) { }
// ✅ CORRECT — constant-time comparison
const encoder = new TextEncoder();
const a = encoder.encode(providedToken);
const b = encoder.encode(env.SECRET_TOKEN);
if (a.byteLength !== b.byteLength) {
return new Response("Forbidden", { status: 403 });
}
const isValid = await crypto.subtle.timingSafeEqual(a, b);Never use Math.random() for security
// ❌ WRONG
const token = Math.random().toString(36).slice(2);
// ✅ CORRECT
const token = crypto.randomUUID();
const randomBytes = crypto.getRandomValues(new Uint8Array(32));Always generate Env types with wrangler
# Run before build/typecheck/test
npx wrangler typesClone requests/responses before multiple reads
// Bodies are streams — can only be consumed once
const cloned = request.clone();
const body1 = await request.text();
const body2 = await cloned.text(); // worksUse Queues and Workflows for background work
// ❌ WRONG — blocks response
export default {
async fetch(request, env) {
await sendEmail(env, request); // user waits for this
return new Response("OK");
}
};
// ✅ CORRECT — decouple via Queue
export default {
async fetch(request, env, ctx) {
await env.EMAIL_QUEUE.send({ to: "user@example.com" });
return new Response("OK"); // immediate
}
};Testing with Vitest + Workers pool
// vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.jsonc" },
},
},
},
});// my-worker.test.ts
import { env } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("Worker", () => {
it("returns OK", async () => {
const response = await fetch("http://localhost/");
expect(response.status).toBe(200);
});
it("can use KV", async () => {
await env.KV.put("test-key", "test-value");
const value = await env.KV.get("test-key");
expect(value).toBe("test-value");
});
});⚠️ Vitest pool auto-injects
nodejs_compat— always verify yourwrangler.jsoncincludes it if your code depends on Node.js modules.
passThroughOnException for fail-open
export default {
async fetch(request, env, ctx) {
ctx.passThroughOnException(); // fail open to origin
// ... risky operation ...
}
};Security headers pattern
function addSecurityHeaders(response: Response): Response {
const headers = new Headers(response.headers);
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
headers.set("X-Content-Type-Options", "nosniff");
headers.set("X-Frame-Options", "DENY");
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
headers.set("Content-Security-Policy", "default-src 'self'; script-src 'self'");
return new Response(response.body, { status: response.status, headers });
}D1 query patterns
// ✅ Always use parameterized queries — never interpolate user input
const user = await env.DB.prepare("SELECT * FROM users WHERE id = ?")
.bind(userId)
.first<User>();
// ✅ Batch for transactional writes
await env.DB.batch([
env.DB.prepare("INSERT INTO sessions (id, user_id) VALUES (?, ?)").bind(sessionId, userId),
env.DB.prepare("UPDATE users SET last_login = ? WHERE id = ?").bind(Date.now(), userId),
]);Durable Objects: use SQL storage for complex state
export class MyDO extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Initialize schema once
this.ctx.blockConcurrencyWhile(async () => {
await this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at INTEGER DEFAULT (unixepoch())
)
`);
});
}
async getItem(id: string): Promise<string | null> {
const row = this.ctx.storage.sql
.exec("SELECT value FROM items WHERE id = ?", id)
.one<{ value: string }>();
return row?.value ?? null;
}
}27. Platform Limits Reference
| Resource | Free | Paid (Workers Paid) |
|---|---|---|
| Requests / day | 100,000 | Unlimited (billed) |
| CPU time per request | 10 ms | 30s (up to 5 min with Durable Objects) |
| Memory per isolate | 128 MB | 128 MB |
| Script size (compressed) | 1 MB | 10 MB |
| Subrequests per request | 50 | 1,000 |
| KV reads / day | 100,000 | Billed |
| KV writes / day | 1,000 | Billed |
| KV value size | 25 MB | 25 MB |
| D1 row reads / day | 5M | Billed |
| R2 operations | — | Billed |
| Durable Object requests | — | Billed |
| WebSocket message max | 32 MiB | 32 MiB |
waitUntil max duration |
30s after response | 30s after response |
| Worker-to-Worker (service binding) calls / request | 32 | 32 |
28. Wrangler Configuration Reference
// wrangler.jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-05-01",
"compatibility_flags": ["nodejs_compat"],
// KV Namespaces
"kv_namespaces": [
{ "binding": "KV", "id": "abc123" }
],
// D1 Databases
"d1_databases": [
{ "binding": "DB", "database_name": "my-db", "database_id": "def456" }
],
// R2 Buckets
"r2_buckets": [
{ "binding": "BUCKET", "bucket_name": "my-bucket" }
],
// Durable Objects
"durable_objects": {
"bindings": [
{ "name": "MY_DO", "class_name": "MyDO" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
],
// AI
"ai": { "binding": "AI" },
// Queues
"queues": {
"producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }],
"consumers": [{ "queue": "my-queue", "max_batch_size": 10, "max_batch_timeout": 5 }]
},
// Service Bindings
"services": [
{ "binding": "OTHER", "service": "other-worker", "entrypoint": "MyEntrypoint" }
],
// Vectorize
"vectorize": [
{ "binding": "INDEX", "index_name": "my-index" }
],
// Cron Triggers
"triggers": {
"crons": ["0 * * * *", "*/5 * * * *"]
},
// Rate Limiting
"unsafe": {
"bindings": [
{
"name": "RATE_LIMITER",
"type": "ratelimit",
"namespace_id": "0",
"simple": { "limit": 100, "period": 60 }
}
]
},
// Environment variables (non-secret)
"vars": {
"ENVIRONMENT": "production",
"API_BASE_URL": "https://api.example.com"
},
// Environments
"env": {
"staging": {
"vars": { "ENVIRONMENT": "staging" },
"kv_namespaces": [{ "binding": "KV", "id": "staging-kv-id" }]
}
}
}Quick Reference Card
// ── Entry point ──────────────────────────────────────────────
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { }
} satisfies ExportedHandler<Env>;
// ── Request ───────────────────────────────────────────────────
const url = new URL(request.url);
const method = request.method;
const body = await request.json();
const country = request.cf?.country;
// ── Response ──────────────────────────────────────────────────
new Response("text", { status: 200 })
Response.json({ key: "value" })
Response.redirect("/new-path", 302)
// ── ctx ───────────────────────────────────────────────────────
ctx.waitUntil(promise) // background work
ctx.passThroughOnException() // fail-open
// ── Cache ─────────────────────────────────────────────────────
const cache = caches.default;
await cache.put(request, response);
await cache.match(request);
await cache.delete(request);
// ── HTMLRewriter ──────────────────────────────────────────────
new HTMLRewriter()
.on("a[href]", { element(el) { el.setAttribute("href", "/new"); } })
.transform(response);
// ── WebSocket ─────────────────────────────────────────────────
const { 0: client, 1: server } = new WebSocketPair();
server.accept();
server.send("msg");
new Response(null, { status: 101, webSocket: client });
// ── Crypto ────────────────────────────────────────────────────
crypto.randomUUID()
crypto.getRandomValues(new Uint8Array(32))
await crypto.subtle.timingSafeEqual(a, b)
await crypto.subtle.digest("SHA-256", data)
// ── RPC ───────────────────────────────────────────────────────
import { WorkerEntrypoint, DurableObject, RpcTarget } from "cloudflare:workers";
// ── Node.js ───────────────────────────────────────────────────
import { AsyncLocalStorage } from "node:async_hooks";
import { Buffer } from "node:buffer";
import { createHash } from "node:crypto";Last updated: May 2025 — based on official Cloudflare Workers documentation.