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-10s | N/A (always warm) |
| Memory overhead | ~5MB per isolate | 50-500MB per container | GB+ |
| Isolation | V8 sandbox | OS-level (cgroups, namespaces) | Process boundaries |
| File system | None | Ephemeral /tmp | Full |
| Native binaries | No | Yes | Yes |
| Startup model | New isolate or reuse warm | New container or reuse warm | Already 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,assertwork via compatibility flags.fs,net,child_processdo 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_compatis a compatibility flag, not full Node.js. Libraries that usefs,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 devprovides 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:
request- standardRequestobjectenv- bindings (services, secrets, variables)ctx- execution context, primarily forctx.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.