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-50msWhy Edge Computing?
Benefits
- 1Ultra-low latency: Code runs within 50ms of users
- 2Global distribution: 300+ locations worldwide
- 3Zero cold starts: V8 isolate starts in milliseconds
- 4Unlimited scalability: Automatic scaling
- 5Cost effective: Only pay for actual usage
- 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 worldwideGetting 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.jsonwrangler.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 hourBasic 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 --localGitHub 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
- 1Keep functions small: Fast initialization and execution
- 2Use caching aggressively: KV for data, Cache API for responses
- 3Optimize bundle size: Tree-shaking, minimal dependencies
- 4Handle errors gracefully: Try-catch, fallback responses
- 5Monitor performance: Analytics, logging, metrics
- 6Test locally: Use wrangler dev before deploying
- 7Use environment variables: Never hardcode secrets
- 8Implement rate limiting: Prevent abuse
- 9Enable compression: Reduce response sizes
- 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!