Platform Model

Cloudflare Workers don’t run in containers or VMs. They run in V8 isolates - the same JavaScript engine that powers Chrome. This is a fundamentally different execution model from AWS Lambda or traditional servers.

V8 Isolates vs Containers vs Lambda

V8 Isolates (Workers)Containers (Lambda, Cloud Run)Traditional Server
Cold start~0ms (isolate reuse)100ms-10sN/A (always warm)
Memory overhead~5MB per isolate50-500MB per containerGB+
IsolationV8 sandboxOS-level (cgroups, namespaces)Process boundaries
File systemNoneEphemeral /tmpFull
Native binariesNoYesYes
Startup modelNew isolate or reuse warmNew container or reuse warmAlready running

The key trade-off: isolates start near-instantly and use almost no memory, but you lose filesystem access and native binary execution. You’re running in a browser-like sandbox, not a Linux environment.

The workerd Runtime

workerd is Cloudflare’s open-source Workers runtime. It embeds V8 and implements the Workers API surface. Locally, wrangler dev runs your code on workerd so you get identical behavior in development and production.

What workerd provides:

  • Web standard APIs: fetch, Request, Response, URL, crypto.subtle, TextEncoder, ReadableStream
  • Workers-specific APIs: waitUntil(), caches, HTMLRewriter
  • Node.js compatibility layer: Partial - Buffer, crypto, util, assert work via compatibility flags. fs, net, child_process do not exist.
// wrangler.jsonc - enable Node.js compat
{
  "compatibility_flags": ["nodejs_compat"]
}

What “Not Node.js” Means

Workers look like JavaScript but the runtime is different. These common Node.js patterns don’t work:

// These DO NOT exist in Workers
import fs from "fs";           // No filesystem
import net from "net";         // No raw sockets
import child_process from "child_process"; // No processes

// These work with nodejs_compat flag
import { Buffer } from "buffer";    // OK
import { createHash } from "crypto"; // OK
import { promisify } from "util";    // OK

Gotcha: nodejs_compat is a compatibility flag, not full Node.js. Libraries that use fs, net, or native addons will fail. Always check if a library’s dependencies are Workers-compatible before using it.

Bindings: Dependency Injection for Infrastructure

Bindings are how Workers access other Cloudflare services. Instead of importing SDKs or constructing clients with credentials, the runtime injects service handles into your Worker’s environment.

// env.DB is a D1 binding - injected by the runtime, not imported
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const result = await env.DB.prepare("SELECT * FROM users").all();
    return Response.json(result);
  },
};

// Binding types
interface Env {
  DB: D1Database;           // D1 relational database
  BUCKET: R2Bucket;         // R2 object storage
  CACHE: KVNamespace;       // KV key-value store
  QUEUE: Queue;             // Queue producer
  COUNTER: DurableObjectNamespace; // Durable Object
  AI: Ai;                   // Workers AI inference
}

Bindings are declared in wrangler.jsonc:

{
  "d1_databases": [
    { "binding": "DB", "database_name": "my-db", "database_id": "xxx" }
  ],
  "r2_buckets": [
    { "binding": "BUCKET", "bucket_name": "my-bucket" }
  ],
  "kv_namespaces": [
    { "binding": "CACHE", "id": "xxx" }
  ]
}

Why bindings matter:

  • No credentials in code - the platform handles auth between services
  • Type-safe - TypeScript types for every service
  • Testable - swap bindings in tests with mocks or local implementations
  • Local dev - wrangler dev provides local versions of all bound services

Request Lifecycle

Every Worker invocation starts with an incoming HTTP request and ends with a response. The isolate may be reused across requests, but you cannot rely on global state persisting.

flowchart LR
    A["Client Request"] --> B["CF Edge"]
    B --> C{"Warm Isolate?"}
    C -->|Yes| D["Reuse Isolate"]
    C -->|No| E["New Isolate"]
    D --> F["fetch handler"]
    E --> F
    F --> G["Bindings\n(D1, R2, KV...)"]
    G --> F
    F --> H["Response"]
    H --> A

Minimal Worker: Raw API

Before frameworks like Hono, this is what a Worker looks like at the lowest level:

// src/index.ts - raw Worker with module syntax
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/health") {
      return new Response("ok", { status: 200 });
    }

    if (url.pathname === "/data" && request.method === "POST") {
      const body = await request.json();
      // Use a binding to store data
      await env.DB.prepare("INSERT INTO items (data) VALUES (?)").bind(JSON.stringify(body)).run();
      return Response.json({ stored: true }, { status: 201 });
    }

    return new Response("Not Found", { status: 404 });
  },
};

interface Env {
  DB: D1Database;
}

Three arguments to fetch:

  1. request - standard Request object
  2. env - bindings (services, secrets, variables)
  3. ctx - execution context, primarily for ctx.waitUntil() to run background work after the response is sent

This is the foundation. Frameworks like Hono wrap this API with routing, middleware, and type safety, but every Worker ultimately exports a fetch handler.