What it is
Cloudflare Workers is a serverless runtime that runs JavaScript, TypeScript, Rust and WASM on V8 isolates at the network edge — in 300+ cities, with sub-millisecond cold starts because isolates are not containers. Around the runtime sits a full primitive stack: D1 (serverless SQLite), R2 (S3-compatible object storage with zero egress fees), KV (eventually consistent key-value), Durable Objects (single-instance stateful actors with strong consistency), Queues (durable message queues) and Workers AI (run inference on edge GPUs).
The CLI is Wrangler, the local emulator is Miniflare, and the project scaffolder is create-cloudflare (aka C3). The whole repo (cloudflare/workers-sdk) is dual-licensed Apache-2.0 + MIT and has shipped almost 2,000 releases.
Architecture
A request hits the closest Cloudflare PoP. The router maps the hostname to your Worker, the Worker runs inside a V8 isolate (think a Chrome tab without the chrome — same security boundary, 5ms cold start), and the Worker can read/write the bound resources directly: a D1 query becomes a fetch to the nearest replica, an R2 read becomes a regional object lookup, a Durable Object call routes to the single canonical instance for that ID.
Why the edge model wins for landing pages and APIs:
- No cold start in practice — isolates start in microseconds, not seconds like Lambda containers.
- Free tier is wide — 100k requests/day on Workers, 5GB on R2, 5M reads/day on D1.
- One config file —
wrangler.tomldeclares the Worker, every binding and every secret name. - Local dev is real — Miniflare emulates the entire runtime locally, including D1, R2, KV and Durable Objects.
Install
One command bootstraps a project end to end:
npm create cloudflare@latest
# or
pnpm create cloudflare@latest
# or
yarn create cloudflare@latest
C3 asks for a name, picks a template (Hello World, API, Hono, Astro, Next.js, Remix, SvelteKit, Workers AI…), installs deps and offers to deploy. Wrangler is the CLI you’ll live in after that:
# Add Wrangler to an existing project
npm install --save-dev wrangler
# Auth once
npx wrangler login
# Dev locally on Miniflare
npx wrangler dev
# Deploy globally
npx wrangler deploy
Useful provisioning commands:
npx wrangler d1 create my-db
npx wrangler r2 bucket create my-bucket
npx wrangler kv namespace create MY_KV
npx wrangler secret put RESEND_API_KEY
Configuration
Everything lives in wrangler.toml. Treat it as the source of truth — it’s checked into git, identical across dev and prod, and the only place bindings are declared.
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2026-05-27"
compatibility_flags = ["nodejs_compat"]
# D1 serverless SQLite
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "12345678-1234-1234-1234-123456789012"
# R2 object storage
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"
# KV namespace
[[kv_namespaces]]
binding = "KV_STORE"
id = "abcdef1234567890"
# Durable Object — single-instance stateful actor
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["Counter"]
# Queues
[[queues.producers]]
queue = "email-jobs"
binding = "EMAIL_QUEUE"
# Vars and secrets
[vars]
ENVIRONMENT = "production"
# Secrets are added via: wrangler secret put RESEND_API_KEY
Code examples
Minimal fetch handler — the canonical Workers shape:
export default {
async fetch(request, env, ctx) {
return new Response("Hello, World!");
}
};
D1-backed lead API — this is almost verbatim the pattern behind the Tzahi and Areta lead pipelines. POST a JSON payload, validate, write to D1, trigger an email via Resend.
-- schema.sql
CREATE TABLE leads (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
message TEXT,
source TEXT,
created_at INTEGER DEFAULT (strftime('%s','now'))
);
CREATE INDEX idx_leads_created ON leads(created_at DESC);
-- Apply
npx wrangler d1 execute my-database --file=./schema.sql --remote
// src/index.ts
export interface Env {
DB: D1Database;
RESEND_API_KEY: string;
}
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
if (req.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
const body = await req.json<{
name: string; email: string; phone?: string; message?: string;
}>();
if (!body.email || !body.name) {
return new Response('Missing fields', { status: 400 });
}
const id = crypto.randomUUID();
await env.DB
.prepare('INSERT INTO leads (id, name, email, phone, message, source) VALUES (?, ?, ?, ?, ?, ?)')
.bind(id, body.name, body.email, body.phone ?? null, body.message ?? null, 'landing')
.run();
// Fire-and-forget notification
ctx.waitUntil(
fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'noreply@example.com',
to: 'me@example.com',
subject: `New lead: ${body.name}`,
text: `${body.name} <${body.email}>\n${body.message ?? ''}`
})
})
);
return Response.json({ ok: true, id });
}
};
R2 file upload + signed read:
export default {
async fetch(req: Request, env: { BUCKET: R2Bucket }) {
const url = new URL(req.url);
if (req.method === 'PUT') {
const key = url.pathname.slice(1);
await env.BUCKET.put(key, req.body, {
httpMetadata: { contentType: req.headers.get('content-type') || 'application/octet-stream' }
});
return Response.json({ key });
}
if (req.method === 'GET') {
const obj = await env.BUCKET.get(url.pathname.slice(1));
if (!obj) return new Response('Not found', { status: 404 });
return new Response(obj.body, {
headers: { 'content-type': obj.httpMetadata?.contentType ?? 'application/octet-stream' }
});
}
return new Response('Method Not Allowed', { status: 405 });
}
};
Durable Object — one strongly-consistent counter per room id, perfect for rate-limiters, websocket rooms, collaborative state:
export class Counter {
state: DurableObjectState;
constructor(state: DurableObjectState) { this.state = state; }
async fetch(req: Request) {
let count = (await this.state.storage.get<number>('n')) ?? 0;
count += 1;
await this.state.storage.put('n', count);
return Response.json({ count });
}
}
export default {
async fetch(req: Request, env: { COUNTER: DurableObjectNamespace }) {
const id = env.COUNTER.idFromName('global');
const stub = env.COUNTER.get(id);
return stub.fetch(req);
}
};
Local dev with Miniflare
The biggest quality-of-life win in the Workers ecosystem is that wrangler dev spins up a full local simulator — Miniflare — that emulates D1, R2, KV, Durable Objects, Queues and Workers AI on disk. No Docker, no signup required. The same wrangler.toml drives both local and production; the binding names you write in code resolve to the local emulator in dev and to the real edge services in deploy. State persists between runs in .wrangler/state/, which means your D1 dev database survives restarts.
# Run locally with live-reload + persistent local state
npx wrangler dev
# Run against real remote bindings (still local code, but talks to prod D1/R2)
npx wrangler dev --remote
# Tail production logs
npx wrangler tail
What’s new / version
Recent shifts that mattered:
- Wrangler 4.x — faster startup, leaner output, cleaner TOML-first config.
- D1 GA — serverless SQLite is now production-stable with read replication and time-travel restore up to 30 days.
- R2 egress-free — pay for storage and ops, never for bandwidth out. This is the killer feature vs S3.
- Workers AI — run Llama, Stable Diffusion XL, Whisper and Mistral models on edge GPUs with one binding.
- workerd open-sourced — the actual Workers runtime (also used by Miniflare) is Apache-2.0; you can self-host if you want.
Why it matters / where I use it
This is the “Landing Page Leads Stack” that I now reuse for every small landing page: Vercel for the static site + Cloudflare Worker + D1 for storage + Resend for email, HMAC-signed end to end. It shipped live on Tzahi on 2026-05-25 and is the same recipe behind Areta and the AE Moments lead pipeline. Free tier covers months of real traffic, deployment is one command, and there’s no Docker, no VPC, no Lambda warmup, no cold-start tax.
Where it stops being the right answer: anywhere you need long-running computation (Workers cap at 30s CPU on free, 5min on paid), heavy ML model weights, or strict POSIX file semantics. For those, a real VM or Supabase Edge wins. For everything else — forms, APIs, webhooks, cron, scheduled jobs, ID rate limits — Workers is the path I keep choosing.