Skip to main content
← All Articles · · 14 min read

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-workersedge-computingserverlesskv-storagedurable-objectsjavascripttypescript

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:

  1. 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.
  2. 300+ PoPs worldwide — Code runs in the datacenter closest to your user, not in us-east-1.
  3. 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)

LimitFree PlanWorkers Paid ($5/month)
Requests/day100,00010 million included
CPU time/request10 ms30 seconds
Memory128 MB128 MB
Script size1 MB10 MB (compressed)
Subrequests/request501,000
Cron triggersNoYes

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

MetricCloudflare WorkersLambda@EdgeVercel Edge Functions
Cold start0-5 ms100-500 ms~5-15 ms
PoP locations300+~450 (CloudFront)~100
RuntimeV8 isolateNode.js/PythonV8 isolate (Next.js)
Memory limit128 MB10 GB128 MB
Execution timeUp to 30 secUp to 30 secUp to 30 sec
WebSocket supportYes (DO)NoNo
Stateful storageKV, DO, R2, D1None (external)Limited (via Vercel)
Pricing$0.50/million req$0.60/million reqIncluded 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.toml and 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

  1. Install Wrangler: npm install -g wrangler
  2. Authenticate: wrangler login
  3. Create project: npm create cloudflare@latest
  4. Add bindings in wrangler.toml (KV namespace, D1 database)
  5. Use TypeScript — the Env interface catches binding errors at compile time
  6. Set secrets: wrangler secret put API_KEY
  7. Deploy: wrangler deploy

Free Download

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.