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

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 cachingfetch results can be cached automatically via cf: { 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:

  1. 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.
  2. Connection limits — most Postgres instances cap at ~100-200 concurrent connections; Workers can spawn thousands. Hyperdrive multiplexes.
  3. 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:

ModuleStatus
node:assertYes
node:async_hooksYes (AsyncLocalStorage)
node:bufferYes (Buffer class)
node:cryptoYes (Node-style crypto on top of WebCrypto)
node:dnsYes
node:eventsYes (EventEmitter)
node:netYes (Socket — limited)
node:pathYes
node:processYes (process.env, process.versions, etc.)
node:streamYes
node:string_decoderYes
node:tlsYes
node:urlYes
node:utilYes
node:zlibYes
node:fsNo (no filesystem)
node:child_processNo (no process spawning)
node:cluster / node:worker_threadsNo
node:http / node:httpsLimited (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, Headers
  • URL, URLPattern, URLSearchParams
  • TextEncoder, TextDecoder
  • crypto (WebCrypto)
  • console (logs flow to wrangler tail)
  • caches (per-POP Cache API)
  • WebSocketPair
  • Blob, File, FormData
  • ReadableStream, WritableStream, TransformStream
  • setTimeout, setInterval, clearTimeout, clearInterval
  • atob, btoa
  • structuredClone
  • AbortController, AbortSignal
  • EventTarget, Event, CustomEvent
  • WeakRef, FinalizationRegistry

Notably missing without nodejs_compat:

  • process, Buffer, global (Node-style)
  • __dirname, __filename
  • require (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 fetch counts; sometimes each env.KV.get() counts.
  • Script size — 3 MB (Free) / 10 MB (Paid) compressed. Large bundles fail to deploy.

Further reading