Edge computing moves computation closer to end users, dramatically reducing latency. This guide explores edge platforms, use cases, and implementation patterns.
Understanding Edge Computing#
Traditional cloud functions run in specific regions. Edge functions run globally, close to your users:
Traditional: User (Tokyo) ──────────────────> Server (US-East)
~200ms latency
Edge: User (Tokyo) ──> Edge Node (Tokyo)
~20ms latency
Edge Platforms Compared#
Cloudflare Workers#
1// worker.js
2export default {
3 async fetch(request, env, ctx) {
4 const url = new URL(request.url);
5
6 // Handle API routes
7 if (url.pathname.startsWith('/api/')) {
8 return handleAPI(request, env);
9 }
10
11 // Serve static assets from R2
12 return env.ASSETS.fetch(request);
13 },
14};
15
16async function handleAPI(request, env) {
17 const data = await env.KV.get('cached-data', 'json');
18
19 if (!data) {
20 // Fetch from origin and cache
21 const fresh = await fetch('https://api.origin.com/data');
22 const json = await fresh.json();
23 await env.KV.put('cached-data', JSON.stringify(json), {
24 expirationTtl: 3600,
25 });
26 return Response.json(json);
27 }
28
29 return Response.json(data);
30}Vercel Edge Functions#
1// app/api/location/route.ts
2import { geolocation } from '@vercel/functions';
3import { NextResponse } from 'next/server';
4
5export const runtime = 'edge';
6
7export async function GET(request: Request) {
8 const geo = geolocation(request);
9
10 return NextResponse.json({
11 country: geo.country,
12 city: geo.city,
13 region: geo.region,
14 latitude: geo.latitude,
15 longitude: geo.longitude,
16 });
17}Deno Deploy#
1// main.ts
2Deno.serve(async (request: Request) => {
3 const url = new URL(request.url);
4
5 if (url.pathname === '/api/time') {
6 return Response.json({
7 timestamp: Date.now(),
8 region: Deno.env.get('DENO_REGION'),
9 });
10 }
11
12 return new Response('Not Found', { status: 404 });
13});Common Edge Use Cases#
1. Geolocation-Based Routing#
1export default {
2 async fetch(request) {
3 const country = request.cf.country;
4
5 const priceMap = {
6 US: { currency: 'USD', modifier: 1 },
7 GB: { currency: 'GBP', modifier: 0.79 },
8 EU: { currency: 'EUR', modifier: 0.92 },
9 JP: { currency: 'JPY', modifier: 149.5 },
10 };
11
12 const pricing = priceMap[country] || priceMap.US;
13
14 return Response.json({
15 country,
16 pricing,
17 });
18 },
19};2. A/B Testing at the Edge#
1export default {
2 async fetch(request, env) {
3 const url = new URL(request.url);
4
5 // Consistent bucketing based on user ID
6 const userId = request.headers.get('x-user-id') ||
7 crypto.randomUUID();
8
9 const bucket = hashToBucket(userId, 2); // 0 or 1
10
11 // Rewrite to variant
12 if (url.pathname === '/landing') {
13 url.pathname = bucket === 0 ? '/landing-a' : '/landing-b';
14 }
15
16 const response = await fetch(url.toString());
17 const newResponse = new Response(response.body, response);
18
19 // Track variant for analytics
20 newResponse.headers.set('x-variant', bucket.toString());
21
22 return newResponse;
23 },
24};
25
26function hashToBucket(str, buckets) {
27 let hash = 0;
28 for (let i = 0; i < str.length; i++) {
29 hash = ((hash << 5) - hash) + str.charCodeAt(i);
30 hash = hash & hash;
31 }
32 return Math.abs(hash) % buckets;
33}3. Authentication at the Edge#
1import { jwtVerify } from 'jose';
2
3export const runtime = 'edge';
4
5export async function middleware(request: Request) {
6 const token = request.headers.get('authorization')?.split(' ')[1];
7
8 if (!token) {
9 return new Response('Unauthorized', { status: 401 });
10 }
11
12 try {
13 const secret = new TextEncoder().encode(process.env.JWT_SECRET);
14 const { payload } = await jwtVerify(token, secret);
15
16 // Add user info to headers for downstream
17 const requestHeaders = new Headers(request.headers);
18 requestHeaders.set('x-user-id', payload.sub as string);
19 requestHeaders.set('x-user-role', payload.role as string);
20
21 return NextResponse.next({
22 request: { headers: requestHeaders },
23 });
24 } catch {
25 return new Response('Invalid token', { status: 401 });
26 }
27}4. Image Optimization#
1export default {
2 async fetch(request, env) {
3 const url = new URL(request.url);
4
5 if (!url.pathname.startsWith('/images/')) {
6 return fetch(request);
7 }
8
9 // Parse optimization params
10 const width = url.searchParams.get('w') || 'auto';
11 const quality = url.searchParams.get('q') || '80';
12 const format = request.headers.get('accept')?.includes('webp')
13 ? 'webp'
14 : 'jpeg';
15
16 // Use Cloudflare Image Resizing
17 return fetch(url.origin + url.pathname, {
18 cf: {
19 image: {
20 width: parseInt(width) || undefined,
21 quality: parseInt(quality),
22 format,
23 },
24 },
25 });
26 },
27};5. Rate Limiting#
1export default {
2 async fetch(request, env) {
3 const ip = request.headers.get('cf-connecting-ip');
4 const key = `ratelimit:${ip}`;
5
6 // Use Durable Objects for accurate counting
7 const id = env.RATE_LIMITER.idFromName(key);
8 const limiter = env.RATE_LIMITER.get(id);
9
10 const allowed = await limiter.fetch(request.url).then(r => r.json());
11
12 if (!allowed.success) {
13 return new Response('Too Many Requests', {
14 status: 429,
15 headers: {
16 'Retry-After': allowed.retryAfter.toString(),
17 },
18 });
19 }
20
21 return fetch(request);
22 },
23};
24
25// Durable Object for rate limiting
26export class RateLimiter {
27 constructor(state, env) {
28 this.state = state;
29 this.requests = [];
30 }
31
32 async fetch(request) {
33 const now = Date.now();
34 const windowMs = 60000; // 1 minute
35 const maxRequests = 100;
36
37 // Clean old requests
38 this.requests = this.requests.filter(t => now - t < windowMs);
39
40 if (this.requests.length >= maxRequests) {
41 const oldestRequest = this.requests[0];
42 const retryAfter = Math.ceil((oldestRequest + windowMs - now) / 1000);
43
44 return Response.json({ success: false, retryAfter });
45 }
46
47 this.requests.push(now);
48 return Response.json({ success: true, remaining: maxRequests - this.requests.length });
49 }
50}Edge Data Storage#
KV Storage (Key-Value)#
1// Write
2await env.MY_KV.put('user:123', JSON.stringify({ name: 'John' }), {
3 expirationTtl: 86400, // 24 hours
4 metadata: { createdAt: Date.now() },
5});
6
7// Read
8const value = await env.MY_KV.get('user:123', 'json');
9const { value: data, metadata } = await env.MY_KV.getWithMetadata('user:123', 'json');
10
11// List
12const list = await env.MY_KV.list({ prefix: 'user:' });Durable Objects (Stateful)#
1export class Counter {
2 constructor(state, env) {
3 this.state = state;
4 }
5
6 async fetch(request) {
7 const url = new URL(request.url);
8 let value = (await this.state.storage.get('count')) || 0;
9
10 switch (url.pathname) {
11 case '/increment':
12 value++;
13 await this.state.storage.put('count', value);
14 break;
15 case '/decrement':
16 value--;
17 await this.state.storage.put('count', value);
18 break;
19 }
20
21 return Response.json({ count: value });
22 }
23}D1 (SQLite at the Edge)#
1export default {
2 async fetch(request, env) {
3 const { results } = await env.DB.prepare(
4 'SELECT * FROM products WHERE category = ?'
5 ).bind('electronics').all();
6
7 return Response.json(results);
8 },
9};Edge Limitations#
Understanding constraints is crucial:
| Constraint | Cloudflare | Vercel Edge | Deno Deploy |
|---|---|---|---|
| CPU time | 10-50ms | 25s | 50ms |
| Memory | 128MB | 128MB | 512MB |
| Bundle size | 1MB | 2MB | No limit |
| Subrequests | 50 | 25 | 1000 |
Working Within Limits#
1// Streaming for large responses
2export default {
3 async fetch(request) {
4 const { readable, writable } = new TransformStream();
5
6 // Don't await - start streaming immediately
7 streamData(writable);
8
9 return new Response(readable, {
10 headers: { 'Content-Type': 'application/json' },
11 });
12 },
13};
14
15async function streamData(writable) {
16 const writer = writable.getWriter();
17 const encoder = new TextEncoder();
18
19 writer.write(encoder.encode('['));
20
21 for (let i = 0; i < 1000; i++) {
22 const data = await fetchItem(i);
23 const separator = i > 0 ? ',' : '';
24 writer.write(encoder.encode(separator + JSON.stringify(data)));
25 }
26
27 writer.write(encoder.encode(']'));
28 writer.close();
29}Deployment Workflow#
1# .github/workflows/edge-deploy.yml
2name: Deploy to Edge
3
4on:
5 push:
6 branches: [main]
7
8jobs:
9 deploy:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v4
13
14 - name: Deploy to Cloudflare Workers
15 uses: cloudflare/wrangler-action@v3
16 with:
17 apiToken: ${{ secrets.CF_API_TOKEN }}
18
19 - name: Smoke Test
20 run: |
21 response=$(curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health)
22 if [ "$response" != "200" ]; then
23 echo "Health check failed"
24 exit 1
25 fiMonitoring Edge Functions#
1// Structured logging
2export default {
3 async fetch(request, env, ctx) {
4 const start = Date.now();
5
6 try {
7 const response = await handleRequest(request, env);
8
9 ctx.waitUntil(log(env, {
10 type: 'request',
11 path: new URL(request.url).pathname,
12 status: response.status,
13 duration: Date.now() - start,
14 colo: request.cf.colo,
15 }));
16
17 return response;
18 } catch (error) {
19 ctx.waitUntil(log(env, {
20 type: 'error',
21 message: error.message,
22 stack: error.stack,
23 }));
24
25 return new Response('Internal Error', { status: 500 });
26 }
27 },
28};Conclusion#
Edge computing enables ultra-low latency for global users. Start with simple use cases like geolocation or caching, then expand to more complex patterns as you understand the constraints. The key is knowing when edge makes sense versus traditional serverless.