Skip to main content
โšก Calmops

Edge Computing with Cloudflare Workers: Complete Guide 2026

Introduction

The future of computing is distributed, and edge computing has evolved from niche technology to essential infrastructure. Cloudflare Workers has emerged as a leading platform for running code at the edge โ€” close to users worldwide โ€” with exceptional performance, global replication, and serverless simplicity.

In 2026, Cloudflare Workers supports not just JavaScript and TypeScript execution, but also persistent storage (D1), stateful computing (Durable Objects), AI inference, and more. This guide covers building production applications with Cloudflare Workers, from basic concepts to advanced patterns.

Understanding Edge Computing

Why Edge?

Edge computing brings computation closer to users:

  • Latency: Sub-millisecond response times by running near users
  • Scalability: Global distribution without capacity planning
  • Cost: Pay only for what you use, with no idle servers
  • Simplicity: Serverless โ€” focus on code, not infrastructure

Cloudflare Workers Overview

Workers run in over 300 data centers worldwide:

User Request โ†’ Nearest Edge Location โ†’ Worker Execution โ†’ Response
                    โ†“
              Global State (D1/Durable Objects)

Getting Started

Your First Worker

// workers/hello-world.js
export default {
  async fetch(request, env, ctx) {
    return new Response("Hello from the edge!", {
      headers: { "content-type": "text/plain" }
    });
  }
};

wrangler.toml Configuration

# wrangler.toml
name = "my-edge-app"
main = "src/index.js"
compatibility_date = "2026-01-15"

# Environment-specific settings
[env.production]
name = "my-edge-app-prod"

# KV Namespace
kv_namespaces = [
  { binding = "CACHE", id = "abc123" }
]

# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "def456"

TypeScript Setup

// src/index.ts
interface Env {
  DB: D1Database;
  CACHE: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    
    if (url.pathname === "/api/users") {
      const { results } = await env.DB.prepare(
        "SELECT * FROM users LIMIT 10"
      ).all();
      
      return Response.json(results);
    }
    
    return new Response("Not Found", { status: 404 });
  }
};

Working with D1 Database

Database Setup

# Create database
wrangler d1 create my-app-db

# Apply schema
wrangler d1 execute my-app-db --local --file=./schema.sql
wrangler d1 execute my-app-db --remote --file=./schema.sql

Schema Definition

-- schema.sql
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  name TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  title TEXT NOT NULL,
  content TEXT,
  published_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_posts_user ON posts(user_id);
CREATE INDEX idx_posts_published ON posts(published_at);

CRUD Operations

// src/db.ts
interface Env {
  DB: D1Database;
}

export class UserRepository {
  constructor(private db: D1Database) {}
  
  async findById(id: number) {
    return this.db.prepare("SELECT * FROM users WHERE id = ?").bind(id).first();
  }
  
  async findByEmail(email: string) {
    return this.db.prepare("SELECT * FROM users WHERE email = ?").bind(email).first();
  }
  
  async create(email: string, name: string) {
    const result = await this.db.prepare(
      "INSERT INTO users (email, name) VALUES (?, ?)"
    ).bind(email, name).run();
    
    return { id: result.meta.last_row_id, email, name };
  }
  
  async update(id: number, data: { email?: string; name?: string }) {
    const updates: string[] = [];
    const bindings: any[] = [];
    
    if (data.email !== undefined) {
      updates.push("email = ?");
      bindings.push(data.email);
    }
    if (data.name !== undefined) {
      updates.push("name = ?");
      bindings.push(data.name);
    }
    
    if (updates.length === 0) return null;
    
    bindings.push(id);
    
    await this.db.prepare(
      `UPDATE users SET ${updates.join(", ")} WHERE id = ?`
    ).bind(...bindings).run();
    
    return this.findById(id);
  }
  
  async delete(id: number) {
    await this.db.prepare("DELETE FROM users WHERE id = ?").bind(id).run();
  }
  
  async list(limit = 10, offset = 0) {
    return this.db.prepare(
      "SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?"
    ).bind(limit, offset).all();
  }
}

Prepared Statements

// For performance, reuse prepared statements
const prepareStatement = env.DB.prepare(
  "SELECT * FROM posts WHERE user_id = ? AND published_at > ? ORDER BY published_at DESC"
);

export async function getUserPosts(db: D1Database, userId: number, since: string) {
  return prepareStatement.bind(userId, since).all();
}

Durable Objects for State

Creating a Durable Object

// src/durable.ts
import { DurableObject } from "cloudflare:workers";

export class GameSession implements DurableObject {
  private state: DurableObjectState;
  private players: Map<string, Player> = new Map();
  
  constructor(state: DurableObjectState, env: Env) {
    this.state = state;
  }
  
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const action = url.pathname;
    
    switch (action) {
      case "/join":
        return this.handleJoin(request);
      case "/move":
        return this.handleMove(request);
      case "/state":
        return this.handleGetState();
      default:
        return new Response("Not Found", { status: 404 });
    }
  }
  
  private async handleJoin(request: Request): Promise<Response> {
    const { playerId, playerName } = await request.json();
    
    this.players.set(playerId, {
      id: playerId,
      name: playerName,
      x: 0,
      y: 0,
      joinedAt: Date.now()
    });
    
    // Persist state
    await this.state.storage.put("players", Array.from(this.players.entries()));
    
    return Response.json({ 
      success: true, 
      playerCount: this.players.size 
    });
  }
  
  private async handleMove(request: Request): Promise<Response> {
    const { playerId, x, y } = await request.json();
    const player = this.players.get(playerId);
    
    if (player) {
      player.x = x;
      player.y = y;
      await this.state.storage.put("players", Array.from(this.players.entries()));
    }
    
    return Response.json({ success: true });
  }
  
  private async handleGetState(): Promise<Response> {
    return Response.json({
      players: Array.from(this.players.values())
    });
  }
}

Accessing Durable Objects

// src/index.ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    
    if (url.pathname.startsWith("/game/")) {
      const gameId = url.pathname.split("/")[2];
      
      // Get the Durable Object stub
      const id = env.GAME_ROOM.idFromName(gameId);
      const stub = env.GAME_ROOM.get(id);
      
      // Forward request to the Durable Object
      return stub.fetch(request);
    }
    
    return new Response("Not Found", { status: 404 });
  }
};

Durable Object Configuration

# wrangler.toml
[durable_objects]
bindings = [
  { name = "GAME_ROOM", class_name = "GameSession" }
]

# Migration for Durable Objects
[[migrations]]
tag = "v1"
new_classes = ["GameSession"]

KV Storage

Basic Operations

// Cache with KV
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const key = url.pathname;
    
    // Try cache first
    const cached = await env.CACHE.get(key);
    if (cached) {
      return new Response(cached, {
        headers: { "x-cache": "HIT" }
      });
    }
    
    // Fetch and cache
    const response = await fetch("https://api.example.com" + key);
    const data = await response.text();
    
    // Cache for 1 hour
    await env.CACHE.put(key, data, { expirationTtl: 3600 });
    
    return new Response(data, {
      headers: { "x-cache": "MISS" }
    });
  }
};

Caching Strategies

class CacheManager {
  constructor(private kv: KVNamespace) {}
  
  async getOrFetch<T>(
    key: string, 
    fetcher: () => Promise<T>,
    ttl: number = 3600
  ): Promise<T> {
    // Check cache
    const cached = await this.kv.get<T>(key, "json");
    if (cached) return cached;
    
    // Fetch fresh data
    const data = await fetcher();
    
    // Store in cache
    await this.kv.put(key, JSON.stringify(data), { 
      expirationTtl: ttl 
    });
    
    return data;
  }
  
  async invalidatePattern(pattern: string): Promise<void> {
    const list = await this.kv.list({ prefix: pattern });
    
    await Promise.all(
      list.keys.map(key => this.kv.delete(key.name))
    );
  }
}

Building APIs

REST API Pattern

// src/api/users.ts
import { UserRepository } from "./db";

export function createUserHandler(db: D1Database) {
  const repo = new UserRepository(db);
  
  return {
    async GET(request: Request): Promise<Response> {
      const url = new URL(request.url);
      const id = url.searchParams.get("id");
      
      if (id) {
        const user = await repo.findById(parseInt(id));
        if (!user) return Response.json({ error: "Not found" }, { status: 404 });
        return Response.json(user);
      }
      
      const limit = parseInt(url.searchParams.get("limit") || "10");
      const offset = parseInt(url.searchParams.get("offset") || "0");
      
      const users = await repo.list(limit, offset);
      return Response.json(users);
    },
    
    async POST(request: Request): Promise<Response> {
      const { email, name } = await request.json();
      
      if (!email) {
        return Response.json({ error: "Email required" }, { status: 400 });
      }
      
      try {
        const user = await repo.create(email, name || "");
        return Response.json(user, { status: 201 });
      } catch (e: any) {
        if (e.message?.includes("UNIQUE constraint")) {
          return Response.json({ error: "Email already exists" }, { status: 409 });
        }
        throw e;
      }
    },
    
    async PUT(request: Request): Promise<Response> {
      const url = new URL(request.url);
      const id = url.searchParams.get("id");
      
      if (!id) {
        return Response.json({ error: "ID required" }, { status: 400 });
      }
      
      const data = await request.json();
      const user = await repo.update(parseInt(id), data);
      
      if (!user) return Response.json({ error: "Not found" }, { status: 404 });
      return Response.json(user);
    },
    
    async DELETE(request: Request): Promise<Response> {
      const url = new URL(request.url);
      const id = url.searchParams.get("id");
      
      if (!id) {
        return Response.json({ error: "ID required" }, { status: 400 });
      }
      
      await repo.delete(parseInt(id));
      return new Response(null, { status: 204 });
    }
  };
}

Router Pattern

// src/router.ts
interface Route {
  method: string;
  path: string;
  handler: (request: Request, env: Env, ctx: ExecutionContext) => Promise<Response>;
}

export class Router {
  private routes: Route[] = [];
  
  get(path: string, handler: Route["handler"]) {
    this.routes.push({ method: "GET", path, handler });
    return this;
  }
  
  post(path: string, handler: Route["handler"]) {
    this.routes.push({ method: "POST", path, handler });
    return this;
  }
  
  put(path: string, handler: Route["handler"]) {
    this.routes.push({ method: "PUT", path, handler });
    return this;
  }
  
  delete(path: string, handler: Route["handler"]) {
    this.routes.push({ method: "DELETE", path, handler });
    return this;
  }
  
  async handle(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    
    for (const route of this.routes) {
      const match = this.matchRoute(route.path, url.pathname);
      
      if (match && route.method === request.method) {
        // Add route params to request
        const req = new Request(request, {
          params: match.params
        });
        return route.handler(req, env, ctx);
      }
    }
    
    return Response.json({ error: "Not Found" }, { status: 404 });
  }
  
  private matchRoute(pattern: string, path: string): { params: Record<string, string> } | null {
    const patternParts = pattern.split("/");
    const pathParts = path.split("/");
    
    if (patternParts.length !== pathParts.length) return null;
    
    const params: Record<string, string> = {};
    
    for (let i = 0; i < patternParts.length; i++) {
      if (patternParts[i].startsWith(":")) {
        params[patternParts[i].slice(1)] = pathParts[i];
      } else if (patternParts[i] !== pathParts[i]) {
        return null;
      }
    }
    
    return { params };
  }
}

Authentication

JWT Verification

// src/auth.ts
import { JWT } from "@cloudflare/jwt-ja";

export async function verifyToken(request: Request, env: Env): Promise<AuthUser | null> {
  const authHeader = request.headers.get("Authorization");
  
  if (!authHeader?.startsWith("Bearer ")) {
    return null;
  }
  
  const token = authHeader.slice(7);
  
  try {
    const jwt = new JWT({
      secret: env.JWT_SECRET
    });
    
    const payload = await jwt.verify(token);
    
    return {
      userId: payload.sub,
      email: payload.email,
      roles: payload.roles || []
    };
  } catch {
    return null;
  }
}

export function requireAuth(handler: (user: AuthUser, ...args: any[]) => Promise<Response>) {
  return async (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> => {
    const user = await verifyToken(request, env);
    
    if (!user) {
      return Response.json({ error: "Unauthorized" }, { status: 401 });
    }
    
    return handler(user, request, env, ctx);
  };
}

Session Management

// src/session.ts
export class SessionManager {
  constructor(private kv: KVNamespace) {}
  
  async create(userId: string, data: SessionData): Promise<string> {
    const sessionId = crypto.randomUUID();
    
    await this.kv.put(sessionId, JSON.stringify({
      userId,
      ...data,
      createdAt: Date.now()
    }), {
      expirationTtl: 86400 // 24 hours
    });
    
    return sessionId;
  }
  
  async get(sessionId: string): Promise<SessionData | null> {
    const data = await this.kv.get(sessionId, "json");
    return data as SessionData | null;
  }
  
  async destroy(sessionId: string): Promise<void> {
    await this.kv.delete(sessionId);
  }
  
  async refresh(sessionId: string, ttl: number = 86400): Promise<void> {
    const session = await this.get(sessionId);
    if (session) {
      await this.kv.put(sessionId, JSON.stringify(session), {
        expirationTtl: ttl
      });
    }
  }
}

AI at the Edge

Running AI Inference

// src/ai.ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    if (request.url.includes("/ai/chat")) {
      return this.handleChat(request, env);
    }
    
    return new Response("Not Found", { status: 404 });
  },
  
  async handleChat(request: Request, env: Env): Promise<Response> {
    const { messages } = await request.json();
    
    // Use Cloudflare AI Gateway
    const response = await fetch(
      "https://gateway.ai.cloudflare.com/v1/account/gateway/chat/completions",
      {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${env.AI_TOKEN}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          model: "@cf/meta/llama-3.1-8b-instruct",
          messages,
          max_tokens: 1024
        })
      }
    );
    
    const data = await response.json();
    return Response.json(data);
  }
};

Performance Optimization

Caching at Edge

// src/cache.ts
export function withCache(
  handler: (request: Request, env: Env) => Promise<Response>
) {
  return async (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> => {
    const url = new URL(request.url);
    
    // Only cache GET requests
    if (request.method !== "GET") {
      return handler(request, env);
    }
    
    const cacheKey = url.pathname + url.search;
    const cached = await env.CACHE.get(cacheKey);
    
    if (cached) {
      return new Response(cached, {
        headers: {
          "CF-Cache-Status": "HIT",
          "Cache-Control": "public, max-age=3600"
        }
      });
    }
    
    const response = await handler(request, env);
    
    if (response.status === 200) {
      const body = await response.text();
      
      await env.CACHE.put(cacheKey, body, {
        expirationTtl: 3600
      });
      
      return new Response(body, {
        headers: {
          "CF-Cache-Status": "MISS",
          "Cache-Control": "public, max-age=3600"
        }
      });
    }
    
    return response;
  };
}

Rate Limiting

// src/rate-limit.ts
export class RateLimiter {
  constructor(private kv: KVNamespace) {}
  
  async check(identifier: string, limit: number, window: number): Promise<boolean> {
    const key = `ratelimit:${identifier}`;
    const current = await this.kv.get(key, "json") as { count: number; resetAt: number } | null;
    
    const now = Date.now();
    
    if (!current || now > current.resetAt) {
      await this.kv.put(key, JSON.stringify({
        count: 1,
        resetAt: now + window * 1000
      }), { expirationTtl: window });
      
      return true;
    }
    
    if (current.count >= limit) {
      return false;
    }
    
    await this.kv.put(key, JSON.stringify({
      count: current.count + 1,
      resetAt: current.resetAt
    }), { expirationTtl: Math.ceil((current.resetAt - now) / 1000) });
    
    return true;
  }
}

Deployment

CI/CD with GitHub Actions

# .github/workflows/deploy.yml
name: Deploy to Cloudflare Workers

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: deploy

Environment Management

# Deploy to production
wrangler deploy --env production

# Deploy to staging
wrangler deploy --env staging

# View deployments
wrangler deployments list

Conclusion

Cloudflare Workers has evolved into a powerful platform for building globally distributed applications. With D1 for storage, Durable Objects for state, and AI inference at the edge, you can build sophisticated applications that run close to your users worldwide.

Start with a simple worker, add a D1 database, then explore Durable Objects for stateful workloads. The pay-per-request model means you only pay for what you use, making it ideal for projects of any size.

The edge is the future, and Cloudflare Workers makes it accessible today.

Resources

Comments