Back to Blog
Edge ComputingServerlessCloudflare WorkersVercel

Edge Computing: Bringing Serverless Closer to Users

Explore edge computing and serverless functions. Learn to deploy code at the edge for ultra-low latency and global performance.

B
Bootspring Team
Engineering
February 26, 2026
7 min read

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:

ConstraintCloudflareVercel EdgeDeno Deploy
CPU time10-50ms25s50ms
Memory128MB128MB512MB
Bundle size1MB2MBNo limit
Subrequests50251000

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 fi

Monitoring 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.

Share this article

Help spread the word about Bootspring