Skip to content
All essays
CraftMarch 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!

Related essays

Next essay
Serverless Architecture Patterns: Event-Driven & Microservices