Introduction
Edge computing is reshaping how we build web applications. Instead of routing every request to a centralized server in a single region, edge computing runs your code at the point closest to the user—on servers distributed across hundreds of data centers worldwide. Cloudflare Workers is one of the most mature and capable edge computing platforms, running JavaScript, TypeScript, and WebAssembly on Cloudflare's global network of over 300 data centers with zero cold starts.
What makes Workers unique is its integration with a comprehensive suite of edge-native storage and compute services. KV provides globally distributed key-value storage for configuration and caching. R2 offers S3-compatible object storage with zero egress fees. D1 brings SQLite to the edge as a serverless database. Durable Objects provide strongly consistent, single-threaded compute for coordination and state management. Together, these primitives enable building full-stack applications that run entirely at the edge.
This guide covers the complete Workers platform, from basic request handling to advanced patterns with Durable Objects, and shows you how to combine these building blocks to build production applications.
Understanding Workers: Core Concepts
Cloudflare Workers run on Cloudflare's V8 isolate-based runtime, similar to browser JavaScript execution. Unlike container-based serverless platforms, Workers don't spin up a container for each request. Instead, they reuse V8 isolates, which start in under 5 milliseconds. This eliminates cold starts entirely—every request, even the first one, responds instantly.
Workers use the Web Standards API model. You write Workers using the same fetch API, Request, Response, and Headers objects you use in browser JavaScript. This means code is portable between Workers, Deno, and browser environments.
The Workers runtime has constraints that differ from Node.js. There's no filesystem access, no native TCP/UDP sockets (though connect() is available), and CPU time is limited per request (typically 10ms on the free plan, 30 seconds on paid). These constraints encourage efficient, stateless code that leverages edge-native storage services.
The platform provides several storage primitives. KV is an eventually consistent global key-value store optimized for read-heavy workloads. R2 is S3-compatible object storage. D1 is a serverless SQLite database. Durable Objects provide single-threaded, strongly consistent storage and compute. Queues enable asynchronous message processing. Vectorize provides vector search for AI applications.
Architecture and Design Patterns
Request Handling with Workers
Workers process HTTP requests through a fetch handler. The handler receives a Request object and returns a Response. Routing, middleware, and error handling are implemented in JavaScript.
// src/index.ts
interface Env {
KV: KVNamespace
R2: R2Bucket
DB: D1Database
SESSIONS: DurableObjectNamespace
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
// CORS middleware
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
})
}
try {
// Route to handlers
if (url.pathname.startsWith('/api/')) {
return handleAPI(request, env, ctx)
}
if (url.pathname.startsWith('/assets/')) {
return handleAsset(request, env)
}
return handlePage(request, env, ctx)
} catch (error) {
return Response.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
}
}
}KV for Configuration and Caching
KV is a global key-value store optimized for read-heavy workloads. It provides low-latency reads from the nearest Cloudflare data center, making it ideal for configuration, feature flags, and caching.
// Configuration management with KV
async function getConfig(env: Env, key: string) {
// KV reads are eventually consistent but very fast (<50ms globally)
const value = await env.KV.get(key, { type: 'json' })
return value
}
async function setConfig(env: Env, key: string, value: any, ttl?: number) {
await env.KV.put(key, JSON.stringify(value), {
expirationTtl: ttl // Optional TTL in seconds
})
}
// Feature flags
async function isFeatureEnabled(env: Env, featureName: string): Promise<boolean> {
const flags = await env.KV.get('feature-flags', { type: 'json' }) as Record<string, boolean>
return flags?.[featureName] ?? false
}R2 for Object Storage
R2 is S3-compatible object storage with zero egress fees. It's ideal for storing files, images, videos, and any unstructured data.
// File upload and serving with R2
async function handleUpload(request: Request, env: Env) {
const formData = await request.formData()
const file = formData.get('file') as File
const key = `uploads/${Date.now()}-${file.name}`
await env.R2.put(key, file.stream(), {
httpMetadata: {
contentType: file.type,
cacheControl: 'public, max-age=31536000'
}
})
return Response.json({ url: `/assets/${key}` })
}
async function handleAsset(request: Request, env: Env) {
const url = new URL(request.url)
const key = url.pathname.replace('/assets/', '')
const object = await env.R2.get(key)
if (!object) return new Response('Not Found', { status: 404 })
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
'Cache-Control': 'public, max-age=31536000',
'ETag': object.etag
}
})
}Durable Objects for Stateful Compute
Durable Objects provide single-threaded, strongly consistent compute with persistent storage. Each Durable Object instance has a unique ID and runs in a single location, ensuring no race conditions.
// Durable Object for a real-time chat room
export class ChatRoom {
state: DurableObjectState
sessions: WebSocket[] = []
constructor(state: DurableObjectState, env: Env) {
this.state = state
}
async fetch(request: Request): Promise<Response> {
if (request.headers.get('Upgrade') === 'websocket') {
const pair = new WebSocketPair()
this.handleWebSocket(pair[1])
return new Response(null, { status: 101, webSocket: pair[0] })
}
// REST API for the chat room
const url = new URL(request.url)
if (url.pathname === '/messages') {
const messages = await this.state.storage.get('messages') || []
return Response.json(messages)
}
return new Response('Not Found', { status: 404 })
}
handleWebSocket(ws: WebSocket) {
ws.accept()
this.sessions.push(ws)
ws.addEventListener('message', async (event) => {
const message = JSON.parse(event.data as string)
// Persist message
const messages = (await this.state.storage.get('messages') as any[]) || []
messages.push({ ...message, timestamp: Date.now() })
await this.state.storage.put('messages', messages)
// Broadcast to all connected clients
for (const session of this.sessions) {
try {
session.send(JSON.stringify(message))
} catch {
this.sessions = this.sessions.filter(s => s !== session)
}
}
})
ws.addEventListener('close', () => {
this.sessions = this.sessions.filter(s => s !== ws)
})
}
}Step-by-Step Implementation
Setting Up a Workers Project
# Create a new Workers project
npm create cloudflare@latest my-worker
cd my-worker
# Install dependencies
npm install
# Start local development
npm run dev
# Deploy to Cloudflare
npm run deployBuilding a Full API with Workers, KV, and D1
// src/api/users.ts
export async function handleUsers(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
const method = request.method
// GET /api/users - List users (cached in KV)
if (method === 'GET' && !url.pathname.includes('/api/users/')) {
const cached = await env.KV.get('users:list', { type: 'json' })
if (cached) return Response.json(cached)
const { results } = await env.DB.prepare(
'SELECT id, name, email FROM users ORDER BY created_at DESC LIMIT 100'
).all()
// Cache in KV for 60 seconds
await env.KV.put('users:list', JSON.stringify(results), { expirationTtl: 60 })
return Response.json(results)
}
// GET /api/users/:id - Get user
if (method === 'GET') {
const id = url.pathname.split('/').pop()
const user = await env.DB.prepare(
'SELECT id, name, email, created_at FROM users WHERE id = ?'
).bind(id).first()
if (!user) return Response.json({ error: 'Not found' }, { status: 404 })
return Response.json(user)
}
// POST /api/users - Create user
if (method === 'POST') {
const body = await request.json() as { name: string; email: string }
const result = await env.DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?) RETURNING id, name, email'
).bind(body.name, body.email).first()
// Invalidate cache
await env.KV.delete('users:list')
return Response.json(result, { status: 201 })
}
return new Response('Method Not Allowed', { status: 405 })
}Implementing Middleware
// src/middleware.ts
export function withAuth(handler: Function) {
return async (request: Request, env: Env, ctx: ExecutionContext) => {
const token = request.headers.get('Authorization')?.replace('Bearer ', '')
if (!token) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
// Verify JWT token
const payload = await verifyJWT(token, env.JWT_SECRET)
if (!payload) {
return Response.json({ error: 'Invalid token' }, { status: 401 })
}
// Add user context to request
ctx.waitUntil(Promise.resolve()) // Keep request alive
return handler(request, env, ctx, payload)
}
}
export function withRateLimit(limit: number, window: number) {
return (handler: Function) => {
return async (request: Request, env: Env, ctx: ExecutionContext) => {
const ip = request.headers.get('CF-Connecting-IP') || 'unknown'
const key = `rate-limit:${ip}`
const current = await env.KV.get(key, { type: 'json' }) as number || 0
if (current >= limit) {
return Response.json(
{ error: 'Rate limit exceeded' },
{ status: 429, headers: { 'Retry-After': String(window) } }
)
}
await env.KV.put(key, String(current + 1), { expirationTtl: window })
return handler(request, env, ctx)
}
}
}Real-World Use Cases
API Gateway at the Edge
Workers serve as an excellent API gateway, handling authentication, rate limiting, caching, and routing before requests reach your origin server. This reduces origin load and improves response times for users worldwide.
Static Site with Dynamic Features
Combine Workers with R2 to serve a static site with dynamic features like A/B testing, personalization, and server-side rendering. The Worker intercepts requests, applies logic, and either serves from R2 or generates a dynamic response.
Real-Time Collaboration
Durable Objects enable real-time collaboration features like shared editing, live cursors, and presence indicators. Each collaboration session is managed by a Durable Object that coordinates state across all connected clients.
Image Processing Pipeline
Use Workers to process images on-the-fly when served from R2. Resize, crop, apply filters, and convert formats at the edge, serving optimized images based on the user's device and network conditions.
Best Practices for Production
-
Minimize cold starts: Workers have virtually no cold starts, but avoid heavy initialization at the module level. Use lazy loading for expensive operations.
-
Use KV for read-heavy data: KV provides the fastest reads but is eventually consistent. Use it for data that can tolerate slight staleness, like configuration and cached responses.
-
Use D1 for relational data: D1 provides SQL queries with strong consistency. Use it for data that requires complex queries, transactions, or full-text search.
-
Use R2 for files: R2's zero egress fees make it ideal for serving files, images, and videos. Use R2's built-in transformations for image optimization.
-
Use Durable Objects for coordination: When you need single-threaded, strongly consistent state, Durable Objects are the right choice. Use them for leaderboards, chat rooms, and collaborative editing.
-
Handle errors gracefully: Workers have strict CPU time limits. Implement timeout handling and fallback logic to prevent requests from failing silently.
-
Cache aggressively: Use the Cache API alongside KV to cache responses at the edge. Set appropriate Cache-Control headers for static assets.
-
Monitor with Analytics: Use Cloudflare Workers Analytics to monitor request counts, error rates, CPU time, and memory usage. Set alerts for anomalies.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Using KV for write-heavy workloads | Slow writes, consistency issues | Use D1 or Durable Objects for write-heavy patterns |
| Exceeding CPU time limits | Request termination | Optimize algorithms, use async operations |
| Not handling Durable Object errors | Lost state, crashes | Implement error handling and state recovery |
| Ignoring eventual consistency in KV | Stale data served to users | Use D1 for strong consistency, KV for cache |
| Storing large objects in KV | Slow reads, storage costs | Use R2 for files, KV for small metadata |
| Not using Cache API | Unnecessary origin requests | Cache responses with appropriate TTLs |
| Durable Object memory limits | OOM crashes | Offload large data to R2 or D1 |
| Missing CORS headers | Frontend can't call API | Add CORS headers to all API responses |
Performance Optimization
Workers performance is primarily limited by CPU time and the number of external calls. Minimize external calls by batching operations and using the waitUntil API for non-blocking work.
// Optimize with waitUntil for background tasks
async function handleRequest(request: Request, env: Env, ctx: ExecutionContext) {
const response = await generateResponse(request, env)
// Don't await logging—let it run in the background
ctx.waitUntil(logRequest(request, env))
return response
}
// Batch KV reads
async function getMultipleConfigs(env: Env, keys: string[]) {
const promises = keys.map(key => env.KV.get(key, { type: 'json' }))
return Promise.all(promises)
}Use the Cache API to serve stale-while-revalidate responses, serving cached content immediately while refreshing it in the background.
Comparison with Alternatives
| Feature | Cloudflare Workers | AWS Lambda@Edge | Vercel Edge Functions | Deno Deploy |
|---|---|---|---|---|
| Cold Start | <5ms (none) | 50-200ms | <50ms | <10ms |
| Runtime | V8 isolates | Node.js | V8 isolates | V8 isolates |
| Edge Locations | 300+ | ~200 (CloudFront) | ~20 (Vercel) | 35+ |
| Built-in Storage | KV, R2, D1, Durable Objects | DynamoDB, S3 | KV, Postgres | KV, DynamoDB |
| Max CPU Time | 30s (paid) | 30s | 30s | 50ms-50s |
| Pricing | Per request | Per request + duration | Per request | Per request |
Advanced Patterns
Edge-Side Rendering
Use Workers to render HTML at the edge, combining static content from R2 with dynamic data from D1 or external APIs.
async function renderPage(request: Request, env: Env) {
const url = new URL(request.url)
const slug = url.pathname.slice(1) || 'home'
// Try cache first
const cacheKey = `page:${slug}`
const cached = await env.KV.get(cacheKey)
if (cached) return new Response(cached, {
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=60' }
})
// Fetch template and data in parallel
const [template, data] = await Promise.all([
env.R2.get(`templates/${slug}.html`),
env.DB.prepare('SELECT * FROM pages WHERE slug = ?').bind(slug).first()
])
const html = renderTemplate(template, data)
await env.KV.put(cacheKey, html, { expirationTtl: 60 })
return new Response(html, { headers: { 'Content-Type': 'text/html' } })
}WebSocket Proxy
Workers can proxy WebSocket connections, adding authentication, routing, and logging at the edge.
Testing Strategies
Use Wrangler's local development mode for unit and integration testing. Test Durable Objects using the unstable_dev API.
import { unstable_dev } from 'wrangler'
describe('API Worker', () => {
let worker: any
beforeAll(async () => {
worker = await unstable_dev('src/index.ts', {
vars: { ENVIRONMENT: 'test' }
})
})
afterAll(async () => await worker.stop())
test('GET /api/users returns user list', async () => {
const res = await worker.fetch('/api/users')
expect(res.status).toBe(200)
const users = await res.json()
expect(Array.isArray(users)).toBe(true)
})
})Future Outlook
Cloudflare is rapidly expanding the Workers platform with new primitives like AI inference (Workers AI), vector search (Vectorize), and workflow orchestration (Workflows). The platform is becoming a comprehensive full-stack development environment that competes with traditional cloud providers.
The edge computing paradigm is shifting from "compute at the edge" to "everything at the edge." With D1, R2, KV, Durable Objects, Queues, and AI, developers can build entire applications without leaving the Cloudflare ecosystem.
Conclusion
Cloudflare Workers provides a powerful platform for building globally distributed applications at the edge. With zero cold starts, integrated storage primitives, and a Web Standards-based API, Workers enables developers to build fast, scalable applications that run close to their users.
Key takeaways:
- Workers eliminate cold starts with V8 isolate-based execution, providing consistent sub-millisecond startup times.
- Choose the right storage primitive for each use case: KV for caching, D1 for relational data, R2 for files, Durable Objects for coordination.
- Leverage edge computing to reduce latency, improve reliability, and lower costs compared to centralized architectures.
- Start building today with Wrangler CLI and Cloudflare's generous free tier.
Refer to the Cloudflare Workers documentation for detailed guides and the D1, KV, R2, and Durable Objects documentation for storage-specific guides.