Skip to content
All essays
ArchitectureMarch 18, 202518 min

Edge Computing with Cloudflare Workers

Build ultra-fast applications at the edge. Master Cloudflare Workers, edge functions, and distributed computing in 2025.

Ü
Ümit Uz
Mobile & Full Stack Developer

Introduction to Edge Computing

Edge computing brings computation closer to users by running code at the network edge. In 2025, Cloudflare Workers has emerged as a leading platform, offering sub-millisecond cold starts and a global network spanning 300+ cities.

Traditional Cloud:
User → CDN → Origin Server (single region)
      ↑         ↑
     200ms    500ms+

Edge Computing:
User → Edge Location (300+ cities)
      ↑
     10-50ms

Why Edge Computing?

Benefits

  1. 1Ultra-low latency: Code runs within 50ms of users
  2. 2Global distribution: 300+ locations worldwide
  3. 3Zero cold starts: V8 isolate starts in milliseconds
  4. 4Unlimited scalability: Automatic scaling
  5. 5Cost effective: Only pay for actual usage
  6. 6Better performance: Faster TTFB and LCP scores
Performance Comparison:
- Traditional Lambda: ~200ms cold start, ~100ms execution
- Cloudflare Worker: ~1ms cold start, ~10ms execution

User Experience:
- US East → US West: 50ms vs 150ms
- Europe → Asia: 100ms vs 300ms
- Global reach: Consistent <50ms worldwide

Getting Started with Cloudflare Workers

Project Setup

bash
# Install Wrangler CLI
npm install -g wrangler

# Create new project
wrangler init my-edge-app

# Directory structure
my-edge-app/
├── src/
│   ├── index.ts          # Main worker
│   ├── handlers/         # Route handlers
│   ├── middleware/       # Auth, logging
│   └── utils/           # Helpers
├── wrangler.toml        # Worker config
├── package.json
└── tsconfig.json

wrangler.toml Configuration

toml
name = "my-edge-app"
main = "src/index.ts"
compatibility_date = "2025-03-01"

# Environment variables
[vars]
ENVIRONMENT = "production"
API_URL = "https://api.example.com"

# Secrets (use wrangler secret)
# wrangler secret put API_KEY

# KV Namespaces
[[kv_namespaces]]
binding = "CACHE"
id = "your-kv-namespace-id"

# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"

# R2 Storage
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"

# Triggers
[triggers]
crons = ["0 * * * *"]  # Every hour

Basic Worker Implementation

Hello World

typescript
// src/index.ts
export interface Env {
  ENVIRONMENT: string;
  API_URL: string;
  CACHE: KVNamespace;
  DB: D1Database;
  BUCKET: R2Bucket;
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    // Handle CORS
    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'
        }
      });
    }

    // Route requests
    const url = new URL(request.url);
    const path = url.pathname;

    if (path === '/api/hello') {
      return Response.json({
        message: 'Hello from the edge!',
        timestamp: Date.now(),
        location: request.cf?.colo
      });
    }

    if (path === '/api/time') {
      return new Response(JSON.stringify({
        time: new Date().toISOString(),
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
      }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }

    return new Response('Not found', { status: 404 });
  }
};

Router Pattern

typescript
// src/router.ts
import { Router } from 'itty-router';

const router = Router();

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    return router.handle(request, env, ctx);
  }
};

// GET /api/users
router.get('/api/users', async (request, env) => {
  const users = await env.DB.prepare(
    'SELECT * FROM users'
  ).all();

  return Response.json(users);
});

// POST /api/users
router.post('/api/users', async (request, env) => {
  const body = await request.json() as { name: string; email: string };

  const result = await env.DB.prepare(
    'INSERT INTO users (name, email) VALUES (?, ?)'
  ).bind(body.name, body.email).run();

  return Response.json({
    id: result.meta.last_row_id,
    ...body
  }, { status: 201 });
});

// GET /api/users/:id
router.get('/api/users/:id', async (request, env) => {
  const { id } = request.params as { id: string };

  const user = await env.DB.prepare(
    'SELECT * FROM users WHERE id = ?'
  ).bind(id).first();

  if (!user) {
    return Response.json({ error: 'User not found' }, { status: 404 });
  }

  return Response.json(user);
});

Advanced Patterns

Edge Caching with KV

typescript
// src/handlers/cache.ts
interface CacheConfig {
  ttl: number;  // Time to live in seconds
  staleWhileRevalidate?: number;
}

export async function getCached<T>(
  key: string,
  fetcher: () => Promise<T>,
  env: Env,
  config: CacheConfig = { ttl: 60 }
): Promise<T> {
  // Try cache first
  const cached = await env.CACHE.get(key, 'json');
  if (cached) {
    console.log('Cache HIT:', key);
    return cached as T;
  }

  console.log('Cache MISS:', key);

  // Fetch fresh data
  const data = await fetcher();

  // Store in cache
  await env.CACHE.put(
    key,
    JSON.stringify(data),
    { expirationTtl: config.ttl }
  );

  return data;
}

// Usage
export const handler = async (request: Request, env: Env) => {
  const { productId } = await request.json();

  const product = await getCached(
    `product:${productId}`,
    async () => {
      // Fetch from origin
      const response = await fetch(`${env.API_URL}/products/${productId}`);
      return response.json();
    },
    env,
    { ttl: 300 }  // 5 minutes
  );

  return Response.json(product);
};

Stale-While-Revalidate

typescript
// src/middleware/swr.ts
export async function staleWhileRevalidate(
  request: Request,
  env: Env
): Promise<Response> {
  const cacheKey = `swr:${request.url}`;

  // Get cached version (even if stale)
  const cached = await env.CACHE.get(cacheKey, 'json');

  if (cached) {
    // Return stale immediately
    const response = Response.json(cached);
    response.headers.set('X-Cache', 'HIT');

    // Revalidate in background
    const revalidate = fetch(request.url)
      .then(res => res.json())
      .then(fresh => env.CACHE.put(cacheKey, JSON.stringify(fresh)));

    // Wait for revalidation but don't block response
    (request as any).waitUntil(revalidate);

    return response;
  }

  // Cache miss - fetch and cache
  const response = await fetch(request.url);
  const data = await response.json();

  await env.CACHE.put(cacheKey, JSON.stringify(data), {
    expirationTtl: 3600  // 1 hour
  });

  return Response.json(data);
}

Edge Database with D1

typescript
// src/handlers/database.ts
export const setupDatabase = async (env: Env) => {
  // Create tables
  await env.DB.exec(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      email TEXT UNIQUE NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    );

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

    CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
    CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id);
  `);
};

// User operations
export const userHandlers = {
  async create(request: Request, env: Env) {
    const { name, email } = await request.json();

    const result = await env.DB.prepare(
      'INSERT INTO users (name, email) VALUES (?, ?)'
    ).bind(name, email).run();

    if (!result.success) {
      return Response.json(
        { error: 'Failed to create user' },
        { status: 500 }
      );
    }

    return Response.json({
      id: result.meta.last_row_id,
      name,
      email
    }, { status: 201 });
  },

  async list(request: Request, env: Env) {
    const url = new URL(request.url);
    const page = parseInt(url.searchParams.get('page') || '1');
    const limit = parseInt(url.searchParams.get('limit') || '10');
    const offset = (page - 1) * limit;

    const users = await env.DB.prepare(`
      SELECT * FROM users
      ORDER BY created_at DESC
      LIMIT ? OFFSET ?
    `).bind(limit, offset).all();

    const total = await env.DB.prepare(
      'SELECT COUNT(*) as count FROM users'
    ).first();

    return Response.json({
      users: users.results,
      pagination: {
        page,
        limit,
        total: total.count,
        totalPages: Math.ceil(total.count / limit)
      }
    });
  },

  async get(request: Request, env: Env) {
    const url = new URL(request.url);
    const id = url.pathname.split('/').pop();

    const user = await env.DB.prepare(
      'SELECT * FROM users WHERE id = ?'
    ).bind(id).first();

    if (!user) {
      return Response.json(
        { error: 'User not found' },
        { status: 404 }
      );
    }

    return Response.json(user);
  }
};

R2 Storage Integration

typescript
// src/handlers/storage.ts
export const storageHandlers = {
  async upload(request: Request, env: Env) {
    const formData = await request.formData();
    const file = formData.get('file') as File;

    if (!file) {
      return Response.json(
        { error: 'No file provided' },
        { status: 400 }
      );
    }

    const key = `uploads/${Date.now()}-${file.name}`;

    await env.BUCKET.put(key, file.stream(), {
      httpMetadata: {
        contentType: file.type
      },
      customMetadata: {
        originalName: file.name,
        uploadedAt: new Date().toISOString()
      }
    });

    return Response.json({
      key,
      url: `https://your-domain.com/${key}`,
      size: file.size,
      type: file.type
    });
  },

  async download(request: Request, env: Env) {
    const url = new URL(request.url);
    const key = url.pathname.replace('/api/files/', '');

    const object = await env.BUCKET.get(key);

    if (!object) {
      return Response.json(
        { error: 'File not found' },
        { status: 404 }
      );
    }

    const headers = new Headers();
    object.writeHttpMetadata(headers);
    headers.set('etag', object.httpEtag);

    return new Response(object.body, { headers });
  },

  async list(request: Request, env: Env) {
    const listed = await env.BUCKET.list({
      limit: 100,
      prefix: 'uploads/'
    });

    return Response.json({
      files: listed.objects.map(obj => ({
        key: obj.key,
        size: obj.size,
        uploaded: obj.uploaded
      }))
    });
  },

  async delete(request: Request, env: Env) {
    const url = new URL(request.url);
    const key = url.pathname.replace('/api/files/', '');

    await env.BUCKET.delete(key);

    return Response.json({ success: true });
  }
};

Authentication at the Edge

JWT Verification

typescript
// src/middleware/auth.ts
import { verify } from 'jsonwebtoken';

interface JWTPayload {
  userId: string;
  email: string;
  role: string;
}

export async function withAuth(
  request: Request,
  env: Env
): Promise<{ user: JWTPayload } | Response> {
  const authHeader = request.headers.get('Authorization');

  if (!authHeader?.startsWith('Bearer ')) {
    return Response.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  const token = authHeader.substring(7);

  try {
    const decoded = verify(token, env.JWT_SECRET) as JWTPayload;

    // Optionally check if user exists in database
    const user = await env.DB.prepare(
      'SELECT id, email, role FROM users WHERE id = ?'
    ).bind(decoded.userId).first();

    if (!user) {
      return Response.json(
        { error: 'User not found' },
        { status: 401 }
      );
    }

    return { user };
  } catch (error) {
    return Response.json(
      { error: 'Invalid token' },
      { status: 401 }
    );
  }
}

// Usage
export const protectedHandler = async (request: Request, env: Env) => {
  const authResult = await withAuth(request, env);

  if (authResult instanceof Response) {
    return authResult;  // Error response
  }

  const { user } = authResult;

  // Access granted
  return Response.json({
    message: 'Protected data',
    user
  });
};

API Key Authentication

typescript
// src/middleware/apiKey.ts
export async function withApiKey(
  request: Request,
  env: Env
): Promise<boolean | Response> {
  const apiKey = request.headers.get('X-API-Key');

  if (!apiKey) {
    return Response.json(
      { error: 'API key required' },
      { status: 401 }
    );
  }

  // Check against KV or D1
  const valid = await env.CACHE.get(`api_key:${apiKey}`);

  if (!valid) {
    return Response.json(
      { error: 'Invalid API key' },
      { status: 403 }
    );
  }

  return true;
}

Performance Optimization

Response Compression

typescript
// src/middleware/compression.ts
export async function withCompression(
  response: Response,
  request: Request
): Promise<Response> {
  const acceptEncoding = request.headers.get('Accept-Encoding') || '';

  // Only compress text-based responses
  const contentType = response.headers.get('Content-Type') || '';
  const shouldCompress = contentType.startsWith('text/') ||
                        contentType.includes('json') ||
                        contentType.includes('xml');

  if (!shouldCompress || !acceptEncoding.includes('gzip')) {
    return response;
  }

  // Compress response
  const originalBody = await response.arrayBuffer();
  const compressedBody = await gzip(originalBody);

  const headers = new Headers(response.headers);
  headers.set('Content-Encoding', 'gzip');
  headers.set('Content-Length', compressedBody.byteLength.toString());

  return new Response(compressedBody, {
    status: response.status,
    headers
  });
}

Image Optimization

typescript
// src/handlers/images.ts
export async function optimizeImage(
  request: Request,
  env: Env
): Promise<Response> {
  const url = new URL(request.url);
  const imageUrl = url.searchParams.get('url');
  const width = parseInt(url.searchParams.get('w') || '800');
  const quality = parseInt(url.searchParams.get('q') || '80');

  if (!imageUrl) {
    return Response.json(
      { error: 'Image URL required' },
      { status: 400 }
    );
  }

  // Fetch original image
  const originalResponse = await fetch(imageUrl);

  if (!originalResponse.ok) {
    return Response.json(
      { error: 'Failed to fetch image' },
      { status: 502 }
    );
  }

  const originalImage = await originalResponse.arrayBuffer();

  // Check if optimized version exists in cache
  const cacheKey = `img:${width}x${quality}:${btoa(imageUrl)}`;
  const cached = await env.CACHE.get(cacheKey, 'arrayBuffer');

  if (cached) {
    return new Response(cached, {
      headers: {
        'Content-Type': 'image/webp',
        'Cache-Control': 'public, max-age=31536000',
        'X-Cache': 'HIT'
      }
    });
  }

  // Optimize image (using sharp or similar)
  const optimized = await sharp(Buffer.from(originalImage))
    .resize(width)
    .webp({ quality })
    .toBuffer();

  // Cache optimized version
  await env.CACHE.put(cacheKey, optimized, {
    expirationTtl: 31536000  // 1 year
  });

  return new Response(optimized, {
    headers: {
      'Content-Type': 'image/webp',
      'Cache-Control': 'public, max-age=31536000',
      'X-Cache': 'MISS'
    }
  });
}

Rate Limiting

typescript
// src/middleware/rateLimit.ts
interface RateLimitConfig {
  requests: number;
  period: number;  // seconds
}

export async function withRateLimit(
  request: Request,
  env: Env,
  config: RateLimitConfig = { requests: 100, period: 60 }
): Promise<boolean | Response> {
  const clientId = request.headers.get('CF-Connecting-IP') || 'unknown';
  const key = `rate_limit:${clientId}`;

  const current = await env.CACHE.get(key);

  if (!current) {
    // First request
    await env.CACHE.put(key, '1', {
      expirationTtl: config.period
    });
    return true;
  }

  const count = parseInt(current);

  if (count >= config.requests) {
    return Response.json(
      { error: 'Rate limit exceeded' },
      {
        status: 429,
        headers: {
          'Retry-After': config.period.toString(),
          'X-RateLimit-Limit': config.requests.toString(),
          'X-RateLimit-Remaining': '0',
          'X-RateLimit-Reset': (Date.now() + config.period * 1000).toString()
        }
      }
    );
  }

  // Increment counter
  await env.CACHE.put(key, (count + 1).toString(), {
    expirationTtl: config.period
  });

  return true;
}

WebSockets with Durable Objects

typescript
// src/durable-objects/chat.ts
export class ChatRoom {
  private state: DurableObjectState;
  private sessions: Set<WebSocket> = new Set();
  private messages: Array<{ user: string; text: string; time: number }> = [];

  constructor(state: DurableObjectState) {
    this.state = state;
    this.state.blockConcurrencyWhile(async () => {
      const stored = await this.state.storage.get('messages');
      if (stored) this.messages = stored as any;
    });
  }

  async fetch(request: Request) {
    const url = new URL(request.url);

    if (url.pathname === '/connect') {
      return this.handleWebSocket(request);
    }

    if (url.pathname === '/history') {
      return Response.json({ messages: this.messages });
    }

    return new Response('Not found', { status: 404 });
  }

  private handleWebSocket(request: Request): Response {
    const pair = new WebSocketPair();

    const [client, server] = Object.values(pair);

    server.accept();

    this.sessions.add(server);

    server.addEventListener('message', (event) => {
      const data = JSON.parse(event.data as string);

      const message = {
        user: data.user,
        text: data.text,
        time: Date.now()
      };

      this.messages.push(message);

      // Persist to storage
      this.state.storage.put('messages', this.messages);

      // Broadcast to all clients
      this.broadcast(message);
    });

    server.addEventListener('close', () => {
      this.sessions.delete(server);
    });

    return new Response(null, { status: 101, webSocket: client });
  }

  private broadcast(message: any) {
    const data = JSON.stringify(message);

    for (const session of this.sessions) {
      try {
        session.send(data);
      } catch (error) {
        this.sessions.delete(session);
      }
    }
  }
}

Monitoring & Analytics

typescript
// src/middleware/analytics.ts
export async function withAnalytics(
  request: Request,
  env: Env,
  response: Response
): Promise<void> {
  const analytics = {
    url: request.url,
    method: request.method,
    status: response.status,
    userAgent: request.headers.get('User-Agent'),
    country: request.cf?.country,
    colo: request.cf?.colo,
    timestamp: Date.now()
  };

  // Send to analytics service
  fetch('https://analytics.example.com/collect', {
    method: 'POST',
    body: JSON.stringify(analytics),
    headers: { 'Content-Type': 'application/json' }
  }).catch(console.error);

  // Store in D1 for later analysis
  await env.DB.prepare(`
    INSERT INTO analytics (url, method, status, country, colo, timestamp)
    VALUES (?, ?, ?, ?, ?, ?)
  `).bind(
    analytics.url,
    analytics.method,
    analytics.status,
    analytics.country,
    analytics.colo,
    analytics.timestamp
  ).run();
}

Deployment

bash
# Deploy to Cloudflare
wrangler deploy

# Deploy with environment
wrangler deploy --env production

# Deploy specific worker
wrangler deploy my-worker

# View logs
wrangler tail

# Run locally
wrangler dev

# Run production preview
wrangler dev --local

GitHub Actions CI/CD

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

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Deploy to Cloudflare
        run: npx wrangler deploy --env production
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

Best Practices

  1. 1Keep functions small: Fast initialization and execution
  2. 2Use caching aggressively: KV for data, Cache API for responses
  3. 3Optimize bundle size: Tree-shaking, minimal dependencies
  4. 4Handle errors gracefully: Try-catch, fallback responses
  5. 5Monitor performance: Analytics, logging, metrics
  6. 6Test locally: Use wrangler dev before deploying
  7. 7Use environment variables: Never hardcode secrets
  8. 8Implement rate limiting: Prevent abuse
  9. 9Enable compression: Reduce response sizes
  10. 10Optimize images: WebP, responsive sizes

Performance Tips

typescript
// 1. Use Response.json() instead of JSON.stringify()
return Response.json(data);

// 2. Set cache headers
return new Response(data, {
  headers: {
    'Cache-Control': 'public, max-age=3600'
  }
});

// 3. Use waitUntil for background tasks
ctx.waitUntil(sendAnalytics(event));

// 4. Batch database operations
await env.DB.batch([
  env.DB.prepare('INSERT INTO users ...'),
  env.DB.prepare('INSERT INTO posts ...')
]);

// 5. Use streaming for large responses
return new Response(stream);

Conclusion

Edge computing with Cloudflare Workers enables building blazing-fast applications with global distribution. The combination of:

  • Sub-millisecond cold starts
  • 300+ global locations
  • Rich ecosystem (KV, D1, R2, Durable Objects)
  • Excellent developer experience

Makes it the perfect choice for modern serverless applications in 2025.

Start small, iterate fast, and scale globally!

Enjoyed this essay? Share it.

Related essays

Next essay
GraphQL Rate Limiting: Protect Your API from Abuse