MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Cloudflare Workers: Edge Computing for the Web

Deploy JavaScript at the edge: KV storage, Durable Objects, and global distribution.

CloudflareEdge ComputingServerlessJavaScript

By MinhVo

Introduction

The traditional model of web application deployment involves running your server code in one or a few data center regions. When a user in Tokyo makes a request to a server in Virginia, the round-trip latency alone can add 150-200 milliseconds to every request. For modern web applications where performance directly impacts user engagement, conversion rates, and SEO rankings, this latency is unacceptable.

Cloudflare Workers fundamentally changes this model by running your JavaScript at the edge—on over 300 data centers worldwide, within milliseconds of virtually every internet user. Workers use V8 isolates (the same engine that powers Chrome) instead of containers, which means they start in under 5 milliseconds with zero cold starts. Your code runs at the same PoP (Point of Presence) that handles the user's DNS resolution, making edge computing as close to the user as physically possible.

Global edge computing network

This guide covers how to build production web applications on Cloudflare Workers, leveraging KV for global state, Durable Objects for coordination, and the Cache API for performance optimization. You'll learn the patterns that make edge computing practical and the trade-offs that make it different from traditional server-side development.

Understanding the Edge Computing Model

Edge computing with Workers is not just about running your existing Node.js code closer to users. It requires a fundamentally different mental model. Your code runs in isolated V8 contexts that can be spun up and torn down in milliseconds. There's no persistent filesystem, no long-running processes, and no traditional database connections. Instead, you work with edge-native primitives designed for this stateless, distributed execution model.

The V8 isolate model means Workers share the host process with other isolates, similar to how browser tabs share a Chrome process. This is far more efficient than container-based serverless platforms—each isolate uses only a few kilobytes of memory at startup, compared to hundreds of megabytes for a container. The result is true pay-per-invocation pricing with no cold start penalty.

V8 isolate architecture

Workers have constraints that shape your application architecture. The CPU time limit is 10ms on the free plan and up to 30 seconds on paid plans. There's no filesystem access—data must be stored in KV, R2, D1, or Durable Objects. Network access is available but optimized for HTTP fetches. These constraints push you toward efficient, cache-first architectures that leverage edge-native storage.

The key insight is that edge computing excels at read-heavy workloads with global audiences. Configuration delivery, authentication checks, A/B testing, content personalization, and API gateway patterns all benefit enormously from running at the edge. Write-heavy workloads with strong consistency requirements need careful design to work within the eventually consistent model of most edge storage primitives.

Architecture and Design Patterns

Worker Request Lifecycle

Every Worker follows the same request lifecycle. A request arrives at the nearest Cloudflare PoP, your Worker's fetch handler is invoked with the request, and you return a response. The entire lifecycle—from receiving the request to sending the response—must complete within the CPU time limit.

export interface Env {
  CACHE_KV: KVNamespace
  USER_DATA: KVNamespace
  SESSIONS: DurableObjectNamespace
  ASSETS: R2Bucket
}
 
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url)
    
    // Edge-native middleware: authentication check at the edge
    const session = await validateSession(request, env)
    if (!session && isProtectedRoute(url.pathname)) {
      return Response.redirect('/login', 302)
    }
    
    // Route handling
    switch (url.pathname) {
      case '/api/config':
        return handleConfig(env)
      case '/api/user':
        return handleUser(request, env, session)
      default:
        return handlePage(request, env, ctx, session)
    }
  }
}

KV: Global Key-Value Storage

KV is Cloudflare's globally distributed key-value store. It's optimized for read-heavy workloads with eventual consistency. Writes are propagated globally within seconds, but reads from the nearest edge location return in under 10 milliseconds. KV is perfect for configuration, feature flags, and cached data that can tolerate brief staleness.

// Feature flag system with KV
async function getFeatureFlags(env: Env): Promise<Record<string, boolean>> {
  const cacheKey = 'feature-flags'
  
  // Try KV first (fastest path)
  const cached = await env.CACHE_KV.get(cacheKey, { type: 'json' })
  if (cached) return cached as Record<string, boolean>
  
  // Fetch from origin and cache in KV
  const response = await fetch('https://api.example.com/flags')
  const flags = await response.json()
  await env.CACHE_KV.put(cacheKey, JSON.stringify(flags), {
    expirationTtl: 60 // Cache for 60 seconds
  })
  
  return flags
}
 
// A/B testing with KV
async function getABVariant(env: Env, userId: string): Promise<string> {
  const key = `ab:${userId}`
  const existing = await env.CACHE_KV.get(key)
  
  if (existing) return existing
  
  // Assign variant (50/50 split)
  const variant = Math.random() < 0.5 ? 'control' : 'treatment'
  await env.CACHE_KV.put(key, variant, { expirationTtl: 86400 })
  return variant
}

Durable Objects: Stateful Coordination

Durable Objects solve the coordination problem in edge computing. While KV is eventually consistent, Durable Objects provide strong consistency by running a single instance of your code for each unique object ID. They're perfect for leaderboards, chat rooms, rate limiters, and any pattern that requires atomic read-modify-write operations.

// Rate limiter Durable Object
export class RateLimiter {
  state: DurableObjectState
  
  constructor(state: DurableObjectState, env: Env) {
    this.state = state
  }
  
  async fetch(request: Request): Promise<Response> {
    const { limit, windowMs } = await request.json() as any
    const now = Date.now()
    
    // Get current state
    const requests = (await this.state.storage.get<number[]>('requests')) || []
    
    // Remove expired entries
    const validRequests = requests.filter(t => now - t < windowMs)
    
    if (validRequests.length >= limit) {
      const oldestValid = validRequests[0]
      const retryAfter = Math.ceil((windowMs - (now - oldestValid)) / 1000)
      return Response.json(
        { allowed: false, retryAfter },
        { status: 429, headers: { 'Retry-After': String(retryAfter) } }
      )
    }
    
    // Record this request
    validRequests.push(now)
    await this.state.storage.put('requests', validRequests)
    
    return Response.json({
      allowed: true,
      remaining: limit - validRequests.length,
      resetAt: new Date(validRequests[0] + windowMs).toISOString()
    })
  }
}
 
// Worker that uses the rate limiter
async function checkRateLimit(request: Request, env: Env): Promise<Response | null> {
  const ip = request.headers.get('CF-Connecting-IP') || 'unknown'
  const id = env.SESSIONS.idFromName(ip)
  const limiter = env.SESSIONS.get(id)
  
  const response = await limiter.fetch('https://internal/rate-limit', {
    method: 'POST',
    body: JSON.stringify({ limit: 100, windowMs: 60000 })
  })
  
  const result = await response.json() as any
  if (!result.allowed) {
    return Response.json({ error: 'Rate limit exceeded' }, { status: 429 })
  }
  return null
}

Edge storage and compute architecture

Step-by-Step Implementation

Project Setup and Configuration

# Create a new Workers project
npm create cloudflare@latest my-edge-app
cd my-edge-app
 
# Project structure
# src/
#   index.ts        # Main worker entry point
#   router.ts       # Request routing
#   middleware.ts    # Edge middleware
#   handlers/       # Route handlers
# wrangler.toml     # Cloudflare configuration
 
# Start development server
npm run dev
# wrangler.toml
name = "my-edge-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"
 
# KV namespaces
[[kv_namespaces]]
binding = "CACHE_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 
[[kv_namespaces]]
binding = "USER_DATA"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 
# Durable Objects
[[durable_objects.bindings]]
name = "SESSIONS"
class_name = "SessionManager"
 
# R2 bucket
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "my-edge-assets"

Authentication at the Edge

// src/middleware/auth.ts
import { SignJWT, jwtVerify } from 'jose'
 
export async function createSession(env: Env, userId: string): Promise<string> {
  const secret = new TextEncoder().encode(env.JWT_SECRET)
  const token = await new SignJWT({ userId, iat: Math.floor(Date.now() / 1000) })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('24h')
    .sign(secret)
  return token
}
 
export async function validateSession(
  request: Request, env: Env
): Promise<{ userId: string } | null> {
  const token = request.headers.get('Cookie')?.match(/session=([^;]+)/)?.[1]
  if (!token) return null
  
  try {
    const secret = new TextEncoder().encode(env.JWT_SECRET)
    const { payload } = await jwtVerify(token, secret)
    return { userId: payload.userId as string }
  } catch {
    return null
  }
}
 
export function requireAuth(handler: Function) {
  return async (request: Request, env: Env, ctx: ExecutionContext) => {
    const session = await validateSession(request, env)
    if (!session) {
      return Response.json({ error: 'Unauthorized' }, { status: 401 })
    }
    return handler(request, env, ctx, session)
  }
}

Serving Static Assets from R2

// src/handlers/assets.ts
export async function handleAsset(request: Request, env: Env): Promise<Response> {
  const url = new URL(request.url)
  const key = url.pathname.slice(1) // Remove leading slash
  
  // Check Cache API first
  const cache = caches.default
  const cacheKey = new Request(request.url, request)
  let response = await cache.match(cacheKey)
  
  if (!response) {
    const object = await env.ASSETS.get(key)
    if (!object) return new Response('Not Found', { status: 404 })
    
    response = new Response(object.body, {
      headers: {
        'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
        'Cache-Control': 'public, max-age=31536000, immutable',
        'ETag': object.etag
      }
    })
    
    // Store in Cache API
    ctx.waitUntil(cache.put(cacheKey, response.clone()))
  }
  
  return response
}

Real-World Use Cases

Global Configuration Distribution

Serving application configuration from the edge eliminates the latency of fetching config from a central server on every page load. Feature flags, API endpoints, client-side configuration, and environment-specific values can all be served from KV with sub-10ms latency globally.

Edge-Side Authentication

Validating JWT tokens at the edge allows you to reject unauthorized requests before they reach your origin server. This reduces origin load and provides faster feedback to users. Combine with KV-cached user profiles for full authentication at the edge.

API Gateway and Aggregation

Workers can serve as an API gateway, routing requests to appropriate microservices, aggregating responses, and caching results. The edge location ensures that the gateway adds minimal latency, and the aggregation logic reduces the number of requests clients need to make.

Content Personalization

Use Workers to personalize content based on user attributes, geolocation, device type, or A/B test variant. Since the personalization logic runs at the edge, there's no additional latency compared to serving static content.

Best Practices for Production

  1. Cache aggressively at the edge: Use the Cache API and KV together to minimize origin requests. Set appropriate TTLs and use stale-while-revalidate patterns for dynamic content.

  2. Design for eventual consistency: KV propagation takes a few seconds. Design your application to handle slightly stale data gracefully. Use Durable Objects when you need strong consistency.

  3. Minimize bundle size: Workers have a 10MB compressed size limit on paid plans and 1MB on free plans. Use tree shaking, avoid unnecessary dependencies, and split large applications into multiple Workers.

  4. Use environment variables for configuration: Store API keys and configuration in Wrangler secrets and environment variables, not in your code. Use KV for runtime configuration that changes without redeployment.

  5. Handle errors gracefully: Network calls from the edge can fail. Implement retry logic, fallback values, and circuit breaker patterns for external service calls.

  6. Monitor and alert: Use Workers Analytics and Logpush to monitor error rates, response times, and request volumes. Set alerts for anomalies.

  7. Test locally with Miniflare: Use Wrangler's built-in Miniflare-based dev server for local testing. It simulates KV, Durable Objects, and R2 locally.

  8. Use Cron Triggers for scheduled tasks: Workers can be triggered on a schedule using Cron Triggers, useful for cache warming, data aggregation, and cleanup tasks.

Common Pitfalls and Solutions

PitfallImpactSolution
Assuming KV is strongly consistentRace conditions, stale dataUse Durable Objects for strong consistency
Large bundle sizesDeployment failuresTree shake, split into multiple Workers
Missing error handling for fetchSilent failuresWrap fetch in try/catch with fallbacks
Not using Cache APIUnnecessary KV/origin callsCache responses with appropriate TTLs
Storing secrets in codeSecurity vulnerabilitiesUse Wrangler secrets
CPU time exceededRequest terminationProfile and optimize hot paths
Durable Object hot spotsUneven loadUse multiple object IDs for load distribution
Global KV write propagation delayBrief inconsistencyDesign for eventual consistency

Performance Optimization

The Cache API is your primary performance tool at the edge. It caches responses at the Cloudflare PoP level, serving subsequent requests without invoking your Worker at all.

// Stale-while-revalidate pattern
async function cachedFetch(request: Request, env: Env, ctx: ExecutionContext) {
  const cache = caches.default
  const cacheKey = new Request(request.url, request)
  
  const cached = await cache.match(cacheKey)
  if (cached) {
    // Check if stale
    const cacheDate = new Date(cached.headers.get('Date') || 0)
    const age = Date.now() - cacheDate.getTime()
    const maxAge = 60000 // 60 seconds
    
    if (age > maxAge) {
      // Serve stale, revalidate in background
      ctx.waitUntil(
        fetch(request).then(response => {
          const headers = new Headers(response.headers)
          headers.set('Cache-Control', 'public, max-age=60')
          const cachedResponse = new Response(response.body, { headers })
          ctx.waitUntil(cache.put(cacheKey, cachedResponse))
        })
      )
    }
    
    return cached
  }
  
  // Cache miss: fetch from origin and cache
  const response = await fetch(request)
  const headers = new Headers(response.headers)
  headers.set('Cache-Control', 'public, max-age=60')
  const cachedResponse = new Response(response.body, { headers })
  ctx.waitUntil(cache.put(cacheKey, cachedResponse.clone()))
  return cachedResponse
}

Comparison with Alternatives

FeatureCloudflare WorkersAWS Lambda@EdgeVercel Edge FunctionsDeno Deploy
Cold Start<5ms (none)50-200ms<50ms<10ms
RuntimeV8 isolatesNode.jsV8 isolatesV8 isolates
Edge Locations300+~200~2035+
Stateful ComputeDurable ObjectsDynamoDBEdge ConfigDeno KV
Free Tier100K req/day1M req/month100K req/day100K req/day
Max CPU Time30s (paid)30s30s50ms-50s
Web StandardsFullLimitedFullFull

Advanced Patterns

Multi-Region Data with Durable Objects

Durable Objects can be configured to run in specific regions, enabling data locality requirements while maintaining global access.

// Pin a Durable Object to a specific region
function getRegionForUser(userId: string): DurableObjectLocationHint {
  // Route users to their nearest region
  return { jurisdiction: 'eu' } // or 'us', 'apac'
}
 
const id = env.SESSIONS.idFromName(userId)
const stub = env.SESSIONS.get(id, getRegionForUser(userId))

Edge Middleware Chain

Build a composable middleware system that processes requests at the edge before they reach your application logic.

type Middleware = (
  request: Request, env: Env, ctx: ExecutionContext, next: () => Promise<Response>
) => Promise<Response>
 
function compose(...middlewares: Middleware[]) {
  return async (request: Request, env: Env, ctx: ExecutionContext) => {
    let index = -1
    
    async function dispatch(i: number): Promise<Response> {
      if (i <= index) throw new Error('next() called multiple times')
      index = i
      const fn = middlewares[i]
      if (!fn) return new Response('Not Found', { status: 404 })
      return fn(request, env, ctx, () => dispatch(i + 1))
    }
    
    return dispatch(0)
  }
}
 
const app = compose(
  corsMiddleware,
  authMiddleware,
  rateLimitMiddleware,
  routerMiddleware
)

Testing Strategies

Test Workers locally using Wrangler's dev server, which provides in-memory implementations of KV, Durable Objects, and R2. For CI/CD, use the unstable_dev API to run integration tests against a local Worker instance.

import { unstable_dev } from 'wrangler'
 
describe('Edge App', () => {
  let worker: any
 
  beforeAll(async () => {
    worker = await unstable_dev('src/index.ts', {
      vars: { JWT_SECRET: 'test-secret' }
    })
  })
 
  afterAll(async () => await worker.stop())
 
  test('protected route requires authentication', async () => {
    const res = await worker.fetch('/api/user')
    expect(res.status).toBe(401)
  })
 
  test('public route returns config', async () => {
    const res = await worker.fetch('/api/config')
    expect(res.status).toBe(200)
    const config = await res.json()
    expect(config).toBeDefined()
  })
})

Future Outlook

Cloudflare is expanding Workers with AI inference (Workers AI), vector search (Vectorize), durable execution (Workflows), and containers (Container Instances). The platform is evolving from an edge compute platform to a full-stack development environment where the entire application—compute, storage, AI, and networking—runs at the edge.

The Web Standards approach means code written for Workers is increasingly portable to other runtimes (Deno, Bun, Node.js with adapters), reducing vendor lock-in while gaining the performance benefits of edge deployment.

Conclusion

Cloudflare Workers enables building web applications that run at the edge, delivering sub-millisecond latency to users worldwide. The platform's V8 isolate model eliminates cold starts, while KV, Durable Objects, and R2 provide the storage primitives needed for full-stack applications.

Key takeaways:

  1. Edge computing excels for read-heavy, globally distributed workloads where latency matters—authentication, configuration, personalization, and API gateway patterns.
  2. KV for eventually consistent reads, Durable Objects for strong consistency—choose the right primitive for each data access pattern.
  3. Cache aggressively at the edge using the Cache API to minimize Worker invocations and origin requests.
  4. Design for the edge model with stateless code, efficient bundle sizes, and graceful error handling for network calls.

Start by identifying the highest-latency parts of your application and moving them to the edge. Refer to the Cloudflare Workers documentation for detailed guides and the Durable Objects documentation for stateful edge patterns.