Cloudflare Workers Complete Guide: KV, Durable Objects & Edge Computing 2026
Complete guide to Cloudflare Workers in 2026. Learn the Workers runtime, KV storage, Durable Objects, R2, D1, and when to choose Workers vs Lambda@Edge vs Vercel Edge Functions.
Cloudflare Workers is no longer just “Lambda but faster at the edge.” In 2026, it’s a complete compute platform with persistent storage (KV, R2, D1), stateful coordination (Durable Objects), real-time communication (WebSockets), and an AI inference layer (Workers AI). If you’re still mentally filing it under “simple edge middleware,” this guide will change how you think about it.
We’ll cover the Workers runtime, all major storage primitives, the Durable Objects model, and a clear comparison with Lambda@Edge and Vercel Edge Functions so you can make the right architectural choice.
Why Cloudflare Workers?
Three reasons Workers wins for specific use cases:
- V8 isolates, not containers — Cold starts are sub-millisecond (0-5 ms) because Workers share a V8 process. Lambda cold starts are 100-500 ms even with SnapStart.
- 300+ PoPs worldwide — Code runs in the datacenter closest to your user, not in
us-east-1. - Unified storage platform — KV, R2, D1, and Durable Objects are all first-party, co-located with your compute.
The trade-off: Workers run in a stripped-down V8 environment with a 128 MB memory limit and no Node.js built-ins. No fs, no native modules, no require. It’s a different programming model.
The Workers Runtime
Creating Your First Worker
npm create cloudflare@latest my-worker
cd my-worker
npm run dev
The worker entrypoint:
// src/index.ts
export interface Env {
MY_KV: KVNamespace;
MY_BUCKET: R2Bucket;
MY_DB: D1Database;
API_SECRET: string; // plain text secret
}
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 });
}
return new Response('Not found', { status: 404 });
},
};
The Env interface is generated from your wrangler.toml bindings. TypeScript checks that you’re using bindings correctly at build time.
Worker Limits (Free vs Paid)
| Limit | Free Plan | Workers Paid ($5/month) |
|---|---|---|
| Requests/day | 100,000 | 10 million included |
| CPU time/request | 10 ms | 30 seconds |
| Memory | 128 MB | 128 MB |
| Script size | 1 MB | 10 MB (compressed) |
| Subrequests/request | 50 | 1,000 |
| Cron triggers | No | Yes |
The 10 ms CPU limit on the free plan is deceptive—it’s CPU time, not wall time. Waiting on a KV read doesn’t count. But CPU-intensive tasks (crypto, image processing) hit this limit quickly.
Workers KV — Distributed Key-Value Storage
KV is a globally replicated key-value store designed for read-heavy workloads. Writes propagate to all regions within ~60 seconds; reads are served from local cache.
Setup
# wrangler.toml
[[kv_namespaces]]
binding = "CACHE"
id = "your-kv-namespace-id"
preview_id = "your-preview-namespace-id"
Basic Operations
// Write
await env.CACHE.put('user:123', JSON.stringify({ name: 'Alice', role: 'admin' }));
// Write with TTL (auto-expire after 3600 seconds)
await env.CACHE.put('session:abc', token, { expirationTtl: 3600 });
// Read
const raw = await env.CACHE.get('user:123');
const user = raw ? JSON.parse(raw) : null;
// Read with metadata
const { value, metadata } = await env.CACHE.getWithMetadata('user:123');
// Delete
await env.CACHE.delete('user:123');
// List keys with prefix
const result = await env.CACHE.list({ prefix: 'user:', limit: 100 });
for (const key of result.keys) {
console.log(key.name, key.expiration, key.metadata);
}
Storing Complex Objects
KV values are strings, ArrayBuffers, or ReadableStreams. For JSON, stringify/parse manually:
interface UserCache {
id: string;
name: string;
permissions: string[];
cachedAt: number;
}
async function getCachedUser(env: Env, userId: string): Promise<UserCache | null> {
const cached = await env.CACHE.get<UserCache>(`user:${userId}`, 'json');
return cached;
}
async function setCachedUser(env: Env, user: UserCache): Promise<void> {
await env.CACHE.put(
`user:${user.id}`,
JSON.stringify(user),
{
expirationTtl: 300, // 5 min cache
metadata: { cachedAt: Date.now() }
}
);
}
When to Use KV vs Not
Good for KV:
- Config values read by every request (feature flags, rate limits)
- User session tokens
- Pre-rendered HTML fragments
- API response caching
Bad for KV:
- Counters or anything requiring atomic increments (use Durable Objects)
- Strong consistency (KV is eventually consistent)
- Large blobs over 25 MB (use R2)
- Relational data (use D1)
Durable Objects — Stateful Edge Compute
Durable Objects are the most powerful and most misunderstood feature of the Workers platform. Each Durable Object is a single-instance JavaScript object that runs in exactly one datacenter at a time. It has its own storage, its own WebSocket connections, and runs in a serialized (single-threaded) execution model.
This makes them perfect for: rate limiting, real-time collaboration, WebSocket servers, distributed locks, and stateful game logic.
Creating a Durable Object
// src/rate-limiter.ts
import { DurableObject } from 'cloudflare:workers';
interface RateLimitState {
count: number;
resetAt: number;
}
export class RateLimiter extends DurableObject {
private state: DurableObjectState;
constructor(state: DurableObjectState, env: Env) {
super(state, env);
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const now = Date.now();
const windowMs = 60_000; // 1 minute window
const limit = 100;
let data: RateLimitState = await this.state.storage.get('ratelimit') ?? {
count: 0,
resetAt: now + windowMs,
};
if (now > data.resetAt) {
// Reset the window
data = { count: 0, resetAt: now + windowMs };
}
if (data.count >= limit) {
return new Response('Rate limit exceeded', {
status: 429,
headers: { 'X-RateLimit-Reset': String(data.resetAt) }
});
}
data.count++;
await this.state.storage.put('ratelimit', data);
return new Response(JSON.stringify({ remaining: limit - data.count }), {
headers: { 'Content-Type': 'application/json' }
});
}
}
Wiring Up the Durable Object
# wrangler.toml
[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiter"
[[migrations]]
tag = "v1"
new_classes = ["RateLimiter"]
// Use from worker
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown';
// Each IP gets its own Durable Object instance
const id = env.RATE_LIMITER.idFromName(ip);
const limiter = env.RATE_LIMITER.get(id);
const limitResponse = await limiter.fetch(request);
if (limitResponse.status === 429) {
return limitResponse;
}
// Continue with actual request handling...
return new Response('OK');
}
};
Real-Time WebSocket Server with Durable Objects
export class ChatRoom extends DurableObject {
private sessions: Set<WebSocket> = new Set();
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();
this.sessions.add(server);
server.addEventListener('message', (event) => {
// Broadcast to all connected clients
for (const session of this.sessions) {
if (session !== server && session.readyState === WebSocket.READY_STATE_OPEN) {
session.send(event.data);
}
}
});
server.addEventListener('close', () => {
this.sessions.delete(server);
});
return new Response(null, { status: 101, webSocket: client });
}
}
Every message goes through a single Durable Object instance—no coordination overhead, no Redis pub/sub required.
D1 — Serverless SQLite at the Edge
D1 is Cloudflare’s serverless SQLite database. It’s not a replacement for Postgres, but for read-heavy workloads it’s remarkably fast because the database runs in the same location as your Worker.
// Query D1
const { results } = await env.DB.prepare(
'SELECT * FROM products WHERE category = ? ORDER BY price ASC LIMIT 20'
).bind('electronics').all();
// Insert with prepared statement
const stmt = env.DB.prepare('INSERT INTO orders (user_id, total, status) VALUES (?, ?, ?)');
await stmt.bind(userId, total, 'pending').run();
// Batch operations (single round trip)
const batchResults = await env.DB.batch([
env.DB.prepare('UPDATE inventory SET stock = stock - 1 WHERE id = ?').bind(productId),
env.DB.prepare('INSERT INTO order_items (order_id, product_id) VALUES (?, ?)').bind(orderId, productId),
]);
D1 with Drizzle ORM:
import { drizzle } from 'drizzle-orm/d1';
const db = drizzle(env.DB);
const products = await db.select().from(productsTable).where(eq(productsTable.category, 'electronics'));
Workers vs Lambda@Edge vs Vercel Edge Functions
Performance Comparison
| Metric | Cloudflare Workers | Lambda@Edge | Vercel Edge Functions |
|---|---|---|---|
| Cold start | 0-5 ms | 100-500 ms | ~5-15 ms |
| PoP locations | 300+ | ~450 (CloudFront) | ~100 |
| Runtime | V8 isolate | Node.js/Python | V8 isolate (Next.js) |
| Memory limit | 128 MB | 10 GB | 128 MB |
| Execution time | Up to 30 sec | Up to 30 sec | Up to 30 sec |
| WebSocket support | Yes (DO) | No | No |
| Stateful storage | KV, DO, R2, D1 | None (external) | Limited (via Vercel) |
| Pricing | $0.50/million req | $0.60/million req | Included in plan |
When to Choose Workers
- Full-stack edge app with storage needs (KV + D1 + R2)
- WebSocket or real-time features
- Custom rate limiting, auth, or A/B testing logic at the edge
- You want the lowest possible cold start globally
- You’re avoiding AWS lock-in
When to Choose Lambda@Edge
- You’re already deep in the AWS ecosystem (CloudFront distributions, ALBs)
- You need Node.js-specific packages that don’t run in V8 isolates
- You need more than 128 MB memory for compute-heavy tasks
- You need Python, Ruby, or other Lambda runtimes
When to Choose Vercel Edge Functions
- You’re building a Next.js App Router application
- You want zero-config deployment with automatic edge routing
- Your team doesn’t want to manage
wrangler.tomland Cloudflare dashboard
Practical Patterns
API Gateway with Auth
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Auth check at the edge — no origin hit for invalid tokens
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 });
}
const token = authHeader.slice(7);
const cachedUser = await env.SESSIONS.get(`session:${token}`, 'json');
if (!cachedUser) {
// Validate against origin, cache result
const validationResponse = await fetch(`${env.AUTH_SERVICE}/validate`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!validationResponse.ok) {
return new Response('Unauthorized', { status: 401 });
}
const user = await validationResponse.json();
await env.SESSIONS.put(`session:${token}`, JSON.stringify(user), { expirationTtl: 900 });
// Add user context to request
return fetch(request, {
headers: { ...Object.fromEntries(request.headers), 'X-User-Id': user.id }
});
}
return fetch(request, {
headers: { ...Object.fromEntries(request.headers), 'X-User-Id': cachedUser.id }
});
}
};
Cron Jobs with Workers
// wrangler.toml
// [[triggers]]
// crons = ["0 */6 * * *"] // every 6 hours
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
ctx.waitUntil(refreshCache(env));
}
};
async function refreshCache(env: Env): Promise<void> {
const data = await fetch('https://api.example.com/products').then(r => r.json());
await env.CACHE.put('products:all', JSON.stringify(data), { expirationTtl: 21600 });
}
Local Development
# Start local dev server with KV, D1, and R2 simulation
npx wrangler dev
# Test Durable Objects locally
npx wrangler dev --local
# Deploy to production
npx wrangler deploy
# Tail logs from production
npx wrangler tail
Wrangler simulates KV, D1, R2, and Durable Objects locally using SQLite. The simulation is close enough for development but has a few edge cases:
- Local KV doesn’t replicate or expire in real-time
- Durable Object storage is a single SQLite file (no distributed behavior)
- R2 operations work but latency doesn’t reflect production
Getting Started Checklist
- Install Wrangler:
npm install -g wrangler - Authenticate:
wrangler login - Create project:
npm create cloudflare@latest - Add bindings in
wrangler.toml(KV namespace, D1 database) - Use TypeScript — the
Envinterface catches binding errors at compile time - Set secrets:
wrangler secret put API_KEY - Deploy:
wrangler deploy
Related Tools
- Cloudflare Workers Playground — Test Workers code in-browser
- KV Storage Calculator — Estimate KV read/write costs
- Cron Expression Builder — Build cron schedules for Worker triggers
- JWT Debugger — Debug auth tokens in your Worker
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links — we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.