Edge Computing Architecture for Frontend Engineers
Edge Computing Architecture for Frontend Engineers
Introduction
The traditional client-server model is fundamentally broken at scale. When your users are distributed across 190 countries, routing every request through a single origin in us-east-1 introduces latency that compounds across every interaction. A user in Mumbai experiencing 280ms round-trip time to Virginia doesn't just wait longer—they abandon your product.
Edge computing isn't about moving your entire application to the edge. It's about strategically distributing compute to where it delivers measurable latency reduction while managing the complexity explosion that comes with globally distributed state.
This blog dissects production edge architectures from first principles: where edge compute makes sense, where it creates more problems than it solves, and how to build systems that leverage edge infrastructure without drowning in distributed systems complexity.
Scale Context
Let's ground this in real production numbers:
| Metric | Value |
|---|---|
| Daily Active Users | 50M |
| Peak Concurrent Users | 2.5M |
| Requests per Second (peak) | 850K |
| P50 Origin Latency | 45ms |
| P99 Origin Latency | 380ms |
| Geographic Distribution | 190 countries |
| Edge PoPs | 300+ |
| CDN Cache Hit Rate | 94% |
| Edge Compute Invocations/day | 12B |
| Average Edge Function Duration | 8ms |
| Cold Start P99 | 50ms |
At this scale, moving 10ms of compute from origin to edge saves 2.4 billion milliseconds of user-waiting-time per day. That's the math that justifies edge complexity.
High-Level Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER DEVICES │
│ [Mobile App] [Web Browser] [IoT Device] [Desktop App] │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ANYCAST DNS LAYER │
│ Routes users to nearest edge PoP based on BGP routing │
│ [Latency-based / GeoDNS fallback] │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ EDGE LAYER │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ EDGE PoP (300+) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ L4 Load │ │ TLS │ │ HTTP/3 │ │ │
│ │ │ Balancer │──│ Termination │──│ QUIC │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ EDGE RUNTIME │ │ │
│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │
│ │ │ │ V8 │ │ WASM │ │ Workers │ │ │ │
│ │ │ │ Isolates │ │ Runtime │ │ KV/DO │ │ │ │
│ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ EDGE CACHE │ │ │
│ │ │ [Static Assets] [API Responses] [HTML Fragments] [KV Data] │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
[Cache Miss / Dynamic Request]
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ORIGIN SHIELD │
│ Consolidates cache misses before hitting origin │
│ [Regional PoPs: US-East, EU-West, APAC] │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ORIGIN │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ API │ │ SSR │ │ Database │ │ Queue │ │
│ │ Gateway │ │ Servers │ │ Cluster │ │ Workers │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Request Lifecycle at the Edge
sequenceDiagram
participant U as User (Mumbai)
participant D as DNS (Anycast)
participant E as Edge PoP (Mumbai)
participant S as Origin Shield (Singapore)
participant O as Origin (Virginia)
U->>D: DNS Query (example.com)
D->>U: Edge PoP IP (Mumbai)
U->>E: HTTPS Request
Note over E: TLS Termination (0-RTT with session resumption)
alt Static Asset (Cache HIT)
E->>U: Cached Response (8ms)
else Edge Compute Required
Note over E: Execute Edge Function
E->>E: V8 Isolate Execution (12ms)
alt Data in Edge KV
E->>E: Read from KV (2ms)
E->>U: Response (22ms total)
else Origin Required
E->>S: Forward to Shield
S->>O: Origin Request
O->>S: Response
S->>E: Shield Response
E->>U: Response (180ms total)
end
end
Edge Runtime Internals
V8 Isolate Architecture
Edge platforms like Cloudflare Workers don't use containers or VMs. They use V8 isolates—lightweight execution contexts that share a single process but maintain memory isolation through V8's security sandbox.
┌─────────────────────────────────────────────────────────────────┐
│ EDGE RUNTIME PROCESS │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ V8 ENGINE ││
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││
│ │ │Isolate A│ │Isolate B│ │Isolate C│ │Isolate D│ ││
│ │ │(Tenant1)│ │(Tenant2)│ │(Tenant1)│ │(Tenant3)│ ││
│ │ │ │ │ │ │ │ │ │ ││
│ │ │ Heap │ │ Heap │ │ Heap │ │ Heap │ ││
│ │ │ 128MB │ │ 128MB │ │ 128MB │ │ 128MB │ ││
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ ││
│ │ ││
│ │ [Shared Code Cache] [JIT Compiler] [GC Threads] ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ [Event Loop] [I/O Threads] [Crypto Hardware Accel] │
└─────────────────────────────────────────────────────────────────┘
Why Isolates Over Containers:
| Aspect | Containers | V8 Isolates |
|---|---|---|
| Cold Start | 300ms-2s | 5-50ms |
| Memory Overhead | 50-200MB | 1-5MB |
| Isolation | Process-level (strong) | V8 sandbox (strong enough) |
| Startup Cost | Full OS init | Parse + Compile |
| Density | 10-50/node | 10,000+/node |
| CPU Limit Granularity | Coarse (cores) | Fine (CPU time) |
The Tradeoff: Isolates sacrifice some isolation guarantees. A malicious tenant can't read another tenant's memory (V8 sandbox prevents this), but they share CPU, network, and timing side-channels. Spectre mitigations add overhead but don't eliminate all side-channel risks.
Cold Start Mechanics
// What happens during edge function cold start
// 1. Isolate Creation (~1ms)
// V8 creates new isolate with fresh heap
// 2. Code Fetch (~2-10ms depending on cache)
// Fetch compiled code from edge cache or KV
// 3. Parse + Compile (~5-30ms for large bundles)
// V8 parses JavaScript, generates bytecode
// Turbofan JIT compiles hot paths (deferred)
// 4. Module Initialization (~1-20ms)
// Top-level code execution
// THIS IS WHERE YOUR CODE RUNS AT COLD START
const config = await fetchConfig(); // BAD: Blocks cold start
const db = new DatabaseClient(); // OK if lazy
export default handler;
// 5. Ready to Handle Request
Cold Start Optimization Strategies:
// BAD: Synchronous top-level work
import { parse } from 'heavy-parser';
const schema = parse(await fetch('/schema.json').then(r => r.text()));
export default {
async fetch(request) {
return new Response(JSON.stringify(schema));
}
};
// GOOD: Lazy initialization with caching
let schema: Schema | null = null;
async function getSchema(): Promise<Schema> {
if (schema) return schema;
// Check KV first (edge-local, ~2ms)
const cached = await KV.get('schema', 'json');
if (cached) {
schema = cached;
return schema;
}
// Fetch and cache
const text = await fetch('/schema.json').then(r => r.text());
schema = parse(text);
await KV.put('schema', JSON.stringify(schema), { expirationTtl: 3600 });
return schema;
}
export default {
async fetch(request) {
const s = await getSchema();
return new Response(JSON.stringify(s));
}
};
Frontend-Specific Edge Patterns
Pattern 1: Edge-Side Rendering (ESR)
Not SSR at origin. Not CSR in browser. Rendering HTML at the edge.
// Edge-Side Rendering with streaming
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Get user context from edge (no origin round-trip)
const geo = request.cf;
const userPrefs = await env.USER_PREFS_KV.get(
request.headers.get('x-user-id') || 'anonymous',
'json'
);
// Stream response
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
// Immediately send shell (critical for LCP)
writer.write(encoder.encode(`
<!DOCTYPE html>
<html lang="${userPrefs?.locale || geo?.country || 'en'}">
<head>
<meta charset="utf-8">
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/app.js" as="script">
<style>${await env.CRITICAL_CSS_KV.get('home')}</style>
</head>
<body>
<div id="shell">${getShell(userPrefs)}</div>
<div id="content">
`));
// Fetch personalized content (can be parallel)
const [recommendations, feed] = await Promise.all([
fetchRecommendations(env, userPrefs, geo),
fetchFeed(env, userPrefs)
]);
// Stream content chunks
writer.write(encoder.encode(renderRecommendations(recommendations)));
writer.write(encoder.encode(renderFeed(feed)));
// Close document
writer.write(encoder.encode(`
</div>
<script src="/app.js" defer></script>
</body>
</html>
`));
writer.close();
return new Response(readable, {
headers: {
'content-type': 'text/html; charset=utf-8',
'cache-control': 'private, max-age=0',
'x-edge-location': geo?.colo || 'unknown'
}
});
}
};
When ESR Makes Sense:
| Use Case | ESR Benefit | Complexity Cost |
|---|---|---|
| Geo-personalized content | Eliminates origin RTT for location | Low |
| A/B testing | Decision at edge, no origin involvement | Low |
| Authentication gating | Validate JWT at edge, fail fast | Medium |
| Personalized recommendations | Requires edge data access | High |
| Full dynamic rendering | Origin parity at edge | Very High |
Pattern 2: Edge-Side Includes (ESI) Revival
ESI is a 2001 spec that's having a renaissance. Assemble pages from cached fragments at the edge.
// Edge fragment assembly
interface Fragment {
key: string;
ttl: number;
fallback?: string;
}
const PAGE_FRAGMENTS: Fragment[] = [
{ key: 'header', ttl: 3600, fallback: '<header>Default Header</header>' },
{ key: 'nav', ttl: 300 },
{ key: 'hero', ttl: 60 },
{ key: 'feed', ttl: 0 }, // Always fresh
{ key: 'footer', ttl: 86400 }
];
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const fragments = await Promise.all(
PAGE_FRAGMENTS.map(async (frag) => {
// Try edge cache first
const cached = await env.FRAGMENT_CACHE.get(frag.key);
if (cached) return { key: frag.key, html: cached };
// Fetch from origin
try {
const res = await fetch(`${env.ORIGIN}/fragments/${frag.key}`, {
cf: { cacheTtl: frag.ttl }
});
const html = await res.text();
// Cache at edge if TTL > 0
if (frag.ttl > 0) {
await env.FRAGMENT_CACHE.put(frag.key, html, {
expirationTtl: frag.ttl
});
}
return { key: frag.key, html };
} catch (e) {
return { key: frag.key, html: frag.fallback || '' };
}
})
);
// Assemble page
const html = assemblePage(fragments);
return new Response(html, {
headers: {
'content-type': 'text/html',
'cache-control': 'private, no-store', // Assembled page not cacheable
'x-fragments': fragments.map(f => f.key).join(',')
}
});
}
};
Pattern 3: Edge State with Durable Objects
When you need consistency at the edge, Durable Objects provide single-threaded execution guarantees.
// Real-time collaboration at the edge
export class DocumentRoom implements DurableObject {
private sessions: Map<string, WebSocket> = new Map();
private document: DocumentState;
private storage: DurableObjectStorage;
constructor(state: DurableObjectState, env: Env) {
this.storage = state.storage;
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (request.headers.get('upgrade') === 'websocket') {
return this.handleWebSocket(request);
}
if (url.pathname === '/state') {
return new Response(JSON.stringify(this.document));
}
return new Response('Not Found', { status: 404 });
}
private async handleWebSocket(request: Request): Promise<Response> {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
const sessionId = crypto.randomUUID();
this.sessions.set(sessionId, server);
server.accept();
// Send current state
server.send(JSON.stringify({
type: 'init',
state: this.document,
sessionId
}));
server.addEventListener('message', async (event) => {
const msg = JSON.parse(event.data as string);
if (msg.type === 'operation') {
// Apply CRDT operation
this.document = applyOperation(this.document, msg.operation);
// Persist to durable storage
await this.storage.put('document', this.document);
// Broadcast to other sessions
this.broadcast(sessionId, {
type: 'operation',
operation: msg.operation,
from: sessionId
});
}
});
server.addEventListener('close', () => {
this.sessions.delete(sessionId);
this.broadcast(sessionId, {
type: 'presence',
action: 'leave',
sessionId
});
});
return new Response(null, { status: 101, webSocket: client });
}
private broadcast(excludeSession: string, message: object) {
const payload = JSON.stringify(message);
for (const [id, socket] of this.sessions) {
if (id !== excludeSession) {
try {
socket.send(payload);
} catch {
this.sessions.delete(id);
}
}
}
}
}
Durable Objects Location Affinity:
User A (London) ──────┐
│
User B (Paris) ───────┼──▶ DO Instance (Frankfurt) ◀──── User C (Berlin)
│
User D (Amsterdam) ───┘
// All users collaborating on same document connect to same DO instance
// DO location chosen based on first accessor, then sticky
// Cross-ocean collaboration adds latency (London→Singapore user joins)
Performance Architecture at the Edge
LCP Optimization Strategy
graph TD
A[Request Arrives at Edge] --> B{Cacheable?}
B -->|Yes| C[Serve from Edge Cache<br/>LCP: 50-100ms]
B -->|No| D{Edge Renderable?}
D -->|Yes| E[Edge-Side Render<br/>LCP: 100-200ms]
D -->|No| F{Shield Cacheable?}
F -->|Yes| G[Serve from Shield<br/>LCP: 150-250ms]
F -->|No| H[Origin Render<br/>LCP: 300-800ms]
C --> I[Stream Critical CSS Inline]
E --> I
G --> I
H --> I
I --> J[Preload LCP Image from Nearest Edge]
J --> K[Priority Hints for Critical Resources]
Edge-Optimized Resource Loading:
// Intelligent preloading at edge based on page type and user context
function generatePreloadHeaders(
pageType: string,
userAgent: string,
connectionType: string
): HeadersInit {
const headers: Record<string, string> = {};
const preloads: string[] = [];
// Critical resources based on page type
const criticalResources = RESOURCE_MAP[pageType] || RESOURCE_MAP.default;
// Adjust for connection quality
const isSlowConnection = ['slow-2g', '2g', '3g'].includes(connectionType);
for (const resource of criticalResources) {
if (isSlowConnection && resource.priority !== 'critical') continue;
preloads.push(
`<${resource.url}>; rel=preload; as=${resource.type}` +
(resource.crossorigin ? '; crossorigin' : '') +
(resource.type === 'fetch' ? '; type="application/json"' : '')
);
}
headers['Link'] = preloads.join(', ');
// Early hints for HTTP/2+ connections
// This triggers 103 Early Hints before full response
if (supportsEarlyHints(userAgent)) {
headers['X-Early-Hints'] = 'true';
}
return headers;
}
// Edge worker implementing Early Hints
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const pageType = detectPageType(url.pathname);
const ua = request.headers.get('user-agent') || '';
const ect = request.headers.get('ect') || '4g';
// Send 103 Early Hints immediately
// Browser starts fetching while we prepare main response
const earlyHints = generatePreloadHeaders(pageType, ua, ect);
// This is Cloudflare-specific API
request.cf?.earlyHints?.(earlyHints);
// Now prepare actual response (might involve origin)
const response = await generateResponse(request, env);
return response;
}
};
Bundle Strategy for Edge Delivery
┌─────────────────────────────────────────────────────────────────────────────┐
│ BUNDLE ARCHITECTURE │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ BUILD TIME │ │
│ │ │ │
│ │ [Source] ──▶ [Bundle] ──▶ [Split] ──▶ [Compress] ──▶ [Upload] │ │
│ │ │ │ │
│ │ ┌─────────┼─────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ [Critical] [Route] [Vendor] │ │
│ │ <15KB Per-page Shared │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ EDGE DISTRIBUTION │ │
│ │ │ │
│ │ [Origin] ──push──▶ [Edge KV] ──replicate──▶ [300+ PoPs] │ │
│ │ │ │
│ │ Replication Strategy: │ │
│ │ • Critical bundles: Eager push to all PoPs │ │
│ │ • Route bundles: Pull-through cache │ │
│ │ • Vendor bundles: Long TTL, immutable │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ RUNTIME DELIVERY │ │
│ │ │ │
│ │ Request ──▶ Edge ──▶ [Check Bundle Manifest] │ │
│ │ │ │ │
│ │ ┌───────────────┼───────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ [Critical] [Route] [Prefetch] │ │
│ │ Inline On-demand Idle callback │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Edge-Aware Code Splitting:
// Build configuration for edge-optimized bundles
// vite.config.ts or similar
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
// Vendor chunks - cached aggressively at edge
if (id.includes('node_modules')) {
if (id.includes('react')) return 'vendor-react';
if (id.includes('lodash')) return 'vendor-lodash';
return 'vendor-misc';
}
// Route-based splitting
const routeMatch = id.match(/\/routes\/([^/]+)/);
if (routeMatch) return `route-${routeMatch[1]}`;
// Shared components
if (id.includes('/components/')) return 'shared-components';
// Default to main bundle
return 'main';
},
// Content-hash for immutable caching
entryFileNames: '[name].[hash].js',
chunkFileNames: '[name].[hash].js',
assetFileNames: '[name].[hash][extname]'
}
}
}
});
// Edge worker for intelligent bundle serving
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname.startsWith('/assets/')) {
return handleAssetRequest(request, env);
}
// HTML request - determine optimal bundles
const route = matchRoute(url.pathname);
const manifest = await env.MANIFEST_KV.get('bundle-manifest', 'json');
// Build resource hints based on route
const criticalChunks = [
manifest.chunks['vendor-react'],
manifest.chunks['main'],
manifest.chunks[`route-${route.name}`]
].filter(Boolean);
const prefetchChunks = route.prefetch?.map(r => manifest.chunks[`route-${r}`]) || [];
const headers = new Headers();
// Preload critical chunks
headers.set('Link', criticalChunks
.map(c => `<${c.url}>; rel=preload; as=script`)
.join(', '));
// Prefetch likely next routes
if (prefetchChunks.length > 0) {
headers.append('Link', prefetchChunks
.map(c => `<${c.url}>; rel=prefetch; as=script`)
.join(', '));
}
// Fetch and return HTML with optimized headers
const html = await generateHTML(route, manifest, env);
return new Response(html, {
headers: {
'Content-Type': 'text/html',
...Object.fromEntries(headers)
}
});
}
};
Edge Data Architecture
KV Store Patterns
Edge KV stores (Cloudflare KV, Fastly KV, etc.) have specific consistency and latency characteristics:
| Property | Cloudflare KV | Durable Objects | Redis (Origin) |
|---|---|---|---|
| Read Latency | 2-10ms | 10-50ms (colocated) | 50-200ms |
| Write Latency | ~60s propagation | Immediate | 5-20ms |
| Consistency | Eventually consistent | Strongly consistent | Strongly consistent |
| Throughput | Very high reads | Limited by single-thread | High |
| Use Case | Config, static data | Coordination, state | Sessions, real-time |
KV Access Patterns:
// Pattern 1: Read-heavy, write-rarely (config, feature flags)
async function getFeatureFlags(env: Env, userId: string): Promise<FeatureFlags> {
const cacheKey = `flags:${userId.slice(0, 2)}`; // Shard by user prefix
let flags = await env.FLAGS_KV.get(cacheKey, 'json');
if (!flags) {
// KV miss - fetch from origin and populate
flags = await fetch(`${env.ORIGIN}/api/flags/${userId}`).then(r => r.json());
// Write to KV (eventually consistent, ~60s propagation)
// Use waitUntil to not block response
env.ctx.waitUntil(
env.FLAGS_KV.put(cacheKey, JSON.stringify(flags), {
expirationTtl: 300, // 5 min TTL
metadata: { populatedAt: Date.now() }
})
);
}
return flags;
}
// Pattern 2: Write-through with origin as source of truth
async function updateUserPreference(
env: Env,
userId: string,
key: string,
value: unknown
): Promise<void> {
// Write to origin first (source of truth)
const res = await fetch(`${env.ORIGIN}/api/users/${userId}/preferences`, {
method: 'PATCH',
body: JSON.stringify({ [key]: value }),
headers: { 'Content-Type': 'application/json' }
});
if (!res.ok) throw new Error('Origin write failed');
// Invalidate KV cache (don't update - let next read repopulate)
await env.USER_PREFS_KV.delete(`prefs:${userId}`);
// Optionally broadcast invalidation to other PoPs
// (Cloudflare KV handles this automatically via eventual consistency)
}
// Pattern 3: Edge-only ephemeral data (rate limiting)
async function checkRateLimit(
env: Env,
identifier: string,
limit: number,
window: number
): Promise<{ allowed: boolean; remaining: number }> {
const key = `rate:${identifier}:${Math.floor(Date.now() / (window * 1000))}`;
// Use KV atomic operations
const current = parseInt(await env.RATE_KV.get(key) || '0', 10);
if (current >= limit) {
return { allowed: false, remaining: 0 };
}
// Increment (eventual consistency OK for rate limiting)
env.ctx.waitUntil(
env.RATE_KV.put(key, String(current + 1), {
expirationTtl: window * 2 // Expire after window passes
})
);
return { allowed: true, remaining: limit - current - 1 };
}
Edge Cache Hierarchy
┌─────────────────────────────────────────────────────────────────────────────┐
│ CACHE HIERARCHY │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ L1: V8 Isolate Memory │ ~100μs │ Per-request │ <1MB │ │ │
│ │ (in-isolate caching) │ │ │ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ L2: Edge Cache API │ ~1ms │ Per-PoP │ ~100GB │ │ │
│ │ (Cloudflare Cache) │ │ │ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ L3: Edge KV │ ~5ms │ Global │ Unlimited │ │ │
│ │ (Eventually Consistent)│ │ (replicated)│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ L4: Origin Shield │ ~50ms │ Regional │ ~1TB │ │ │
│ │ (Tiered Cache) │ │ │ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ L5: Origin │ ~200ms │ Central │ ∞ │ │ │
│ │ (Database/API) │ │ │ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Cache Control Strategy:
// Sophisticated cache control at edge
interface CacheConfig {
edge: number; // Edge PoP cache TTL
browser: number; // Browser cache TTL
staleWhileRevalidate: number;
staleIfError: number;
tags: string[]; // For tag-based invalidation
}
const CACHE_POLICIES: Record<string, CacheConfig> = {
'static-immutable': {
edge: 31536000, // 1 year
browser: 31536000,
staleWhileRevalidate: 0,
staleIfError: 86400,
tags: ['static']
},
'static-versioned': {
edge: 86400, // 1 day at edge
browser: 3600, // 1 hour in browser
staleWhileRevalidate: 86400,
staleIfError: 604800,
tags: ['static', 'versioned']
},
'api-public': {
edge: 60, // 1 minute
browser: 0, // No browser cache
staleWhileRevalidate: 300,
staleIfError: 3600,
tags: ['api']
},
'api-personalized': {
edge: 0, // No edge cache (personalized)
browser: 0,
staleWhileRevalidate: 0,
staleIfError: 60,
tags: ['api', 'personalized']
},
'html-dynamic': {
edge: 0,
browser: 0,
staleWhileRevalidate: 60,
staleIfError: 300,
tags: ['html', 'dynamic']
}
};
function setCacheHeaders(response: Response, policy: CacheConfig): Response {
const headers = new Headers(response.headers);
// Surrogate-Control for edge caches
if (policy.edge > 0) {
headers.set('Surrogate-Control', `max-age=${policy.edge}`);
}
// Cache-Control for browser
let cacheControl = policy.browser > 0
? `public, max-age=${policy.browser}`
: 'private, no-store';
if (policy.staleWhileRevalidate > 0) {
cacheControl += `, stale-while-revalidate=${policy.staleWhileRevalidate}`;
}
if (policy.staleIfError > 0) {
cacheControl += `, stale-if-error=${policy.staleIfError}`;
}
headers.set('Cache-Control', cacheControl);
// Cache tags for purging
if (policy.tags.length > 0) {
headers.set('Cache-Tag', policy.tags.join(','));
}
return new Response(response.body, {
status: response.status,
headers
});
}
Security at the Edge
JWT Validation Without Origin
// Edge JWT validation - no origin round-trip for auth check
import { decode, verify } from '@tsndr/cloudflare-worker-jwt';
interface JWTPayload {
sub: string;
exp: number;
iat: number;
permissions: string[];
}
async function validateJWT(
token: string,
env: Env
): Promise<{ valid: true; payload: JWTPayload } | { valid: false; error: string }> {
try {
// Decode without verification first (fast path for expiry check)
const decoded = decode(token);
if (!decoded.payload) {
return { valid: false, error: 'Invalid token structure' };
}
const payload = decoded.payload as JWTPayload;
// Check expiration (no crypto needed)
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
return { valid: false, error: 'Token expired' };
}
// Get signing key from KV (cached at edge)
// Keys rotated via origin, pushed to KV
const keyId = decoded.header?.kid || 'default';
const signingKey = await env.JWT_KEYS_KV.get(keyId);
if (!signingKey) {
// Key not in edge cache - could fetch from origin
// but safer to reject and let client refresh
return { valid: false, error: 'Unknown signing key' };
}
// Verify signature
const isValid = await verify(token, signingKey, { algorithm: 'RS256' });
if (!isValid) {
return { valid: false, error: 'Invalid signature' };
}
return { valid: true, payload };
} catch (e) {
return { valid: false, error: 'Token validation failed' };
}
}
// Usage in edge worker
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 });
}
const token = authHeader.slice(7);
const result = await validateJWT(token, env);
if (!result.valid) {
return new Response(JSON.stringify({ error: result.error }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
// Attach user context for downstream handlers
const enrichedRequest = new Request(request, {
headers: new Headers([
...request.headers,
['X-User-ID', result.payload.sub],
['X-User-Permissions', result.payload.permissions.join(',')]
])
});
return handleAuthenticatedRequest(enrichedRequest, env);
}
};
Edge Rate Limiting
// Distributed rate limiting using sliding window at edge
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
keyGenerator: (request: Request) => string;
}
class EdgeRateLimiter {
private config: RateLimitConfig;
private env: Env;
constructor(config: RateLimitConfig, env: Env) {
this.config = config;
this.env = env;
}
async check(request: Request): Promise<{
allowed: boolean;
remaining: number;
resetAt: number;
}> {
const key = this.config.keyGenerator(request);
const now = Date.now();
const windowStart = now - this.config.windowMs;
// Use Durable Object for accurate counting
// KV would have race conditions
const id = this.env.RATE_LIMIT_DO.idFromName(key);
const stub = this.env.RATE_LIMIT_DO.get(id);
const response = await stub.fetch('http://internal/check', {
method: 'POST',
body: JSON.stringify({
timestamp: now,
windowStart,
maxRequests: this.config.maxRequests
})
});
return response.json();
}
}
// Durable Object for rate limit state
export class RateLimitDO implements DurableObject {
private storage: DurableObjectStorage;
private timestamps: number[] = [];
constructor(state: DurableObjectState) {
this.storage = state.storage;
// Load existing timestamps
state.blockConcurrencyWhile(async () => {
this.timestamps = await this.storage.get('timestamps') || [];
});
}
async fetch(request: Request): Promise<Response> {
const { timestamp, windowStart, maxRequests } = await request.json();
// Remove expired timestamps
this.timestamps = this.timestamps.filter(t => t > windowStart);
// Check limit
if (this.timestamps.length >= maxRequests) {
const resetAt = this.timestamps[0] + (timestamp - windowStart);
return Response.json({
allowed: false,
remaining: 0,
resetAt
});
}
// Add new timestamp
this.timestamps.push(timestamp);
await this.storage.put('timestamps', this.timestamps);
return Response.json({
allowed: true,
remaining: maxRequests - this.timestamps.length,
resetAt: this.timestamps[0] + (timestamp - windowStart)
});
}
}
WAF at the Edge
// Edge WAF for request filtering
const WAF_RULES: WAFRule[] = [
{
id: 'sql-injection',
patterns: [
/(\%27)|(\')|(\-\-)|(\%23)|(#)/i,
/((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-)|(\%3B)|(;))/i,
/\w*((\%27)|(\'))((\%6F)|o|(\%4F))((\%72)|r|(\%52))/i
],
action: 'block',
inspect: ['query', 'body']
},
{
id: 'xss-basic',
patterns: [
/((\%3C)|<)((\%2F)|\/)*[a-z0-9\%]+((\%3E)|>)/i,
/((\%3C)|<)((\%69)|i|(\%49))((\%6D)|m|(\%4D))((\%67)|g|(\%47))/i
],
action: 'sanitize',
inspect: ['query', 'body']
},
{
id: 'path-traversal',
patterns: [
/\.\.\//,
/\.\.\\/,
/\%2e\%2e\%2f/i,
/\%252e\%252e\%252f/i
],
action: 'block',
inspect: ['path', 'query']
}
];
async function applyWAF(request: Request): Promise<Request | Response> {
const url = new URL(request.url);
const body = request.method !== 'GET' ? await request.text() : '';
const inspectionTargets = {
path: url.pathname,
query: url.search,
body: body
};
for (const rule of WAF_RULES) {
for (const target of rule.inspect) {
const value = inspectionTargets[target as keyof typeof inspectionTargets];
for (const pattern of rule.patterns) {
if (pattern.test(value)) {
// Log for analysis
console.log(`WAF: Rule ${rule.id} triggered on ${target}`);
if (rule.action === 'block') {
return new Response('Forbidden', {
status: 403,
headers: { 'X-WAF-Rule': rule.id }
});
}
if (rule.action === 'sanitize') {
// Sanitize and continue
inspectionTargets[target as keyof typeof inspectionTargets] =
value.replace(pattern, '');
}
}
}
}
}
// Reconstruct request if body was consumed
if (body) {
return new Request(request.url, {
method: request.method,
headers: request.headers,
body: inspectionTargets.body
});
}
return request;
}
Observability at the Edge
Distributed Tracing
// Edge tracing integration
interface TraceContext {
traceId: string;
spanId: string;
parentSpanId?: string;
sampled: boolean;
}
function extractTraceContext(request: Request): TraceContext {
// W3C Trace Context
const traceparent = request.headers.get('traceparent');
if (traceparent) {
const parts = traceparent.split('-');
return {
traceId: parts[1],
spanId: crypto.randomUUID().replace(/-/g, '').slice(0, 16),
parentSpanId: parts[2],
sampled: parts[3] === '01'
};
}
// Generate new trace
return {
traceId: crypto.randomUUID().replace(/-/g, ''),
spanId: crypto.randomUUID().replace(/-/g, '').slice(0, 16),
sampled: Math.random() < 0.01 // 1% sampling
};
}
function createSpan(
ctx: TraceContext,
name: string,
attributes: Record<string, unknown>
): Span {
return {
traceId: ctx.traceId,
spanId: crypto.randomUUID().replace(/-/g, '').slice(0, 16),
parentSpanId: ctx.spanId,
name,
startTime: Date.now(),
attributes: {
...attributes,
'service.name': 'edge-worker',
'deployment.environment': 'production'
}
};
}
// Usage in edge worker
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const traceCtx = extractTraceContext(request);
const rootSpan = createSpan(traceCtx, 'edge.request', {
'http.method': request.method,
'http.url': request.url,
'http.user_agent': request.headers.get('user-agent'),
'cf.colo': request.cf?.colo,
'cf.country': request.cf?.country
});
try {
// Create child span for cache check
const cacheSpan = createSpan(
{ ...traceCtx, spanId: rootSpan.spanId },
'edge.cache.check',
{ 'cache.key': request.url }
);
const cached = await caches.default.match(request);
cacheSpan.endTime = Date.now();
cacheSpan.attributes['cache.hit'] = !!cached;
if (cached) {
rootSpan.endTime = Date.now();
rootSpan.attributes['cache.status'] = 'hit';
// Export spans asynchronously
ctx.waitUntil(exportSpans(env, [rootSpan, cacheSpan]));
return cached;
}
// Origin fetch span
const originSpan = createSpan(
{ ...traceCtx, spanId: rootSpan.spanId },
'edge.origin.fetch',
{ 'http.url': env.ORIGIN + new URL(request.url).pathname }
);
const response = await fetch(env.ORIGIN + new URL(request.url).pathname, {
headers: {
...Object.fromEntries(request.headers),
'traceparent': `00-${traceCtx.traceId}-${rootSpan.spanId}-${traceCtx.sampled ? '01' : '00'}`
}
});
originSpan.endTime = Date.now();
originSpan.attributes['http.status_code'] = response.status;
rootSpan.endTime = Date.now();
rootSpan.attributes['http.status_code'] = response.status;
ctx.waitUntil(exportSpans(env, [rootSpan, cacheSpan, originSpan]));
return response;
} catch (error) {
rootSpan.endTime = Date.now();
rootSpan.attributes['error'] = true;
rootSpan.attributes['error.message'] = error.message;
ctx.waitUntil(exportSpans(env, [rootSpan]));
throw error;
}
}
};
async function exportSpans(env: Env, spans: Span[]): Promise<void> {
// Export to OTLP endpoint
await fetch(env.OTLP_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${env.OTLP_TOKEN}`
},
body: JSON.stringify({
resourceSpans: [{
resource: {
attributes: [
{ key: 'service.name', value: { stringValue: 'edge-worker' } }
]
},
scopeSpans: [{
spans: spans.map(formatOTLPSpan)
}]
}]
})
});
}
Edge Metrics
// Metrics collection at edge
interface Metrics {
requestCount: number;
errorCount: number;
cacheHitCount: number;
cacheMissCount: number;
latencyHistogram: Map<string, number>; // bucket -> count
bytesSent: number;
}
class EdgeMetricsCollector {
private metrics: Metrics = {
requestCount: 0,
errorCount: 0,
cacheHitCount: 0,
cacheMissCount: 0,
latencyHistogram: new Map(),
bytesSent: 0
};
private flushInterval: number = 10000; // 10 seconds
private lastFlush: number = Date.now();
record(measurement: {
latency: number;
status: number;
cacheStatus: 'hit' | 'miss' | 'bypass';
bytes: number;
}): void {
this.metrics.requestCount++;
if (measurement.status >= 500) {
this.metrics.errorCount++;
}
if (measurement.cacheStatus === 'hit') {
this.metrics.cacheHitCount++;
} else if (measurement.cacheStatus === 'miss') {
this.metrics.cacheMissCount++;
}
// Histogram buckets: 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s
const bucket = this.getBucket(measurement.latency);
this.metrics.latencyHistogram.set(
bucket,
(this.metrics.latencyHistogram.get(bucket) || 0) + 1
);
this.metrics.bytesSent += measurement.bytes;
}
private getBucket(latencyMs: number): string {
const buckets = [10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
for (const b of buckets) {
if (latencyMs <= b) return `le_${b}`;
}
return 'le_inf';
}
shouldFlush(): boolean {
return Date.now() - this.lastFlush > this.flushInterval;
}
async flush(env: Env): Promise<void> {
const payload = {
timestamp: Date.now(),
colo: env.CF_COLO,
metrics: this.metrics
};
// Write to Analytics Engine or external metrics system
await env.METRICS_AE.writeDataPoint({
blobs: [JSON.stringify(payload)],
doubles: [
this.metrics.requestCount,
this.metrics.errorCount,
this.metrics.cacheHitCount,
this.metrics.bytesSent
],
indexes: [env.CF_COLO]
});
// Reset
this.metrics = {
requestCount: 0,
errorCount: 0,
cacheHitCount: 0,
cacheMissCount: 0,
latencyHistogram: new Map(),
bytesSent: 0
};
this.lastFlush = Date.now();
}
}
Production Incidents & Lessons
Incident 1: Edge Cache Stampede
Scenario: Cache TTL expired simultaneously for a popular resource. 300 PoPs all sent cache-miss requests to origin at the same moment.
Impact: Origin overwhelmed, 5-minute degradation affecting 2M users.
Timeline:
00:00 - Cache TTL expires globally
00:01 - 300 PoPs send simultaneous requests to origin
00:02 - Origin connection pool exhausted (5000 connections)
00:03 - Origin starts rejecting connections, 503s propagate
00:04 - Error pages cached at edge (TTL: 10s)
00:05 - Stale-while-revalidate kicks in, serving stale content
00:06 - Origin recovers, cache repopulates
Root Cause: Synchronized cache expiration + no request coalescing.
Fix:
// Request coalescing at edge
const inflightRequests = new Map<string, Promise<Response>>();
async function fetchWithCoalescing(
request: Request,
env: Env
): Promise<Response> {
const cacheKey = new URL(request.url).pathname;
// Check if request already in flight
const inflight = inflightRequests.get(cacheKey);
if (inflight) {
// Wait for existing request instead of making new one
const response = await inflight;
return response.clone();
}
// First request - make it and share result
const fetchPromise = (async () => {
try {
const response = await fetch(request);
return response;
} finally {
// Clean up after small delay to catch close-together requests
setTimeout(() => inflightRequests.delete(cacheKey), 100);
}
})();
inflightRequests.set(cacheKey, fetchPromise);
return fetchPromise;
}
// Also add jitter to cache TTLs
function setCacheWithJitter(ttl: number): number {
// Add ±10% jitter
const jitter = ttl * 0.1 * (Math.random() * 2 - 1);
return Math.floor(ttl + jitter);
}
Incident 2: Durable Object Hot Spot
Scenario: Single Durable Object handling all rate limiting for a viral marketing campaign. One DO instance processing 50K requests/second.
Impact: DO instance couldn't keep up, rate limiting became ineffective, origin overwhelmed.
Fix:
// Sharded Durable Objects for hot keys
function getRateLimitDOId(
env: Env,
identifier: string,
shardCount: number = 64
): DurableObjectId {
// Consistent hashing to shard across multiple DOs
const hash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(identifier)
);
const hashArray = new Uint8Array(hash);
const shardIndex = hashArray[0] % shardCount;
// Each shard handles 1/64th of the load
return env.RATE_LIMIT_DO.idFromName(`${identifier}:shard:${shardIndex}`);
}
// Aggregate counts across shards
async function checkRateLimitSharded(
env: Env,
identifier: string,
limit: number
): Promise<boolean> {
const shardCount = 64;
const perShardLimit = Math.ceil(limit / shardCount);
// Only check local shard for this request
// Allows some over-limit (up to shardCount-1 extra) but avoids hot spot
const id = getRateLimitDOId(env, identifier, shardCount);
const stub = env.RATE_LIMIT_DO.get(id);
const response = await stub.fetch('http://internal/check', {
method: 'POST',
body: JSON.stringify({ limit: perShardLimit })
});
return (await response.json()).allowed;
}
Incident 3: Cold Start Cascade
Scenario: Deploy pushed new worker code, evicted all V8 isolates. Next minute saw P99 latency spike from 50ms to 800ms.
Impact: 15% of requests experienced cold start simultaneously, SLA breach.
Fix:
// Pre-warming strategy
// Deploy to canary PoPs first, let them warm up
// Then deploy to remaining PoPs in waves
// Also: minimize cold start work
// Before (bad):
const heavyDependency = await import('heavy-lib');
const config = JSON.parse(await fetch('/config').then(r => r.text()));
// After (good):
let heavyDependency: typeof import('heavy-lib') | null = null;
let config: Config | null = null;
async function getHeavyDep() {
if (!heavyDependency) {
heavyDependency = await import('heavy-lib');
}
return heavyDependency;
}
async function getConfig(env: Env): Promise<Config> {
if (!config) {
// KV read is faster than origin fetch during cold start
config = await env.CONFIG_KV.get('config', 'json');
}
return config!;
}
// Handler doesn't pay cold start cost until deps actually needed
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Simple paths don't need heavy deps
if (url.pathname === '/health') {
return new Response('ok');
}
// Only load when actually needed
const config = await getConfig(env);
// ...
}
};
Tradeoffs & Engineering Decisions
Edge vs Origin Compute
| Factor | Edge Compute | Origin Compute |
|---|---|---|
| Latency | 5-50ms | 100-500ms |
| CPU Time Limit | 10-50ms typical | Unlimited |
| Memory Limit | 128MB typical | GB+ |
| State Access | KV (eventually consistent), DO (consistent but limited) | Full database |
| Debugging | Limited (logs only) | Full debugging |
| Cost | Per-request + CPU time | Per-instance |
| Cold Start | 5-50ms | N/A (warm instances) |
| Use Cases | Auth, routing, personalization, caching | Complex business logic, transactions |
Decision Framework:
┌─────────────────────────┐
│ Request arrives │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ Can edge handle alone? │
│ - Static asset? │
│ - Cached response? │
│ - Simple transform? │
└───────────┬─────────────┘
Yes / No
┌──────────┴──────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Edge handles │ │ Needs state? │
│ ~10ms │ └───────┬───────┘
└───────────────┘ Yes / No
┌────────┴────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Edge KV/DO │ │ Simple origin │
│ sufficient? │ │ call? │
└───────┬───────┘ └───────┬───────┘
Yes / No Yes / No
┌──────┴──────┐ ┌──────┴──────┐
▼ ▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│Edge + KV/ │ │Edge proxy │ │ Origin │
│DO ~20ms │ │ to origin │ │ compute │
└───────────┘ │ ~100ms │ └───────────┘
└───────────┘
Consistency vs Latency
Eventually Consistent (Edge KV):
- Feature flags: OK (minutes of staleness acceptable)
- User preferences: OK (user doesn't notice)
- Rate limiting: Partial OK (approximate is fine)
- Session tokens: NOT OK (security risk)
- Inventory counts: NOT OK (overselling)
Strongly Consistent (Durable Objects):
- Collaborative editing: Required
- Distributed locks: Required
- Counter increments: Required
- But: Single-threaded, location-bound latency
Hybrid Pattern:
// Read from edge KV (fast, eventually consistent)
// Write through origin (consistent)
// Invalidate edge cache on write
async function getUserProfile(env: Env, userId: string) {
// Try edge first (2ms)
const cached = await env.USER_KV.get(`profile:${userId}`, 'json');
if (cached) return cached;
// Fall back to origin (200ms)
const profile = await fetch(`${env.ORIGIN}/users/${userId}`).then(r => r.json());
// Cache at edge for next request
env.ctx.waitUntil(
env.USER_KV.put(`profile:${userId}`, JSON.stringify(profile), {
expirationTtl: 300
})
);
return profile;
}
async function updateUserProfile(env: Env, userId: string, updates: object) {
// Write to origin first (source of truth)
const response = await fetch(`${env.ORIGIN}/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(updates)
});
if (!response.ok) throw new Error('Update failed');
// Invalidate edge cache
await env.USER_KV.delete(`profile:${userId}`);
return response.json();
}
Future Evolution
Emerging Patterns
-
AI at the Edge: Running inference models (ONNX, TFLite) at edge for real-time personalization without origin latency.
-
WASM Everywhere: Rust/Go compiled to WASM running at edge with near-native performance, enabling complex compute without JavaScript limitations.
-
Edge Databases: SQLite at edge (Cloudflare D1, Turso) enabling SQL queries without origin round-trips for read-heavy workloads.
-
Streaming SSR: React Server Components rendered at edge, streaming HTML chunks with Suspense boundaries.
-
Edge-Native Frameworks: Frameworks built for edge-first (not adapted from Node.js), understanding isolate limitations from the ground up.
// Future: AI inference at edge
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const image = await request.arrayBuffer();
// Run ML model at edge (Cloudflare Workers AI)
const result = await env.AI.run('@cf/microsoft/resnet-50', {
image: [...new Uint8Array(image)]
});
// Personalize response based on image classification
const category = result.label;
const recommendations = await env.RECS_KV.get(`category:${category}`, 'json');
return Response.json({ category, recommendations });
}
};
Summary
Edge computing for frontend isn't about moving everything to the edge—it's about strategic placement of compute where it delivers measurable user experience improvements while managing distributed systems complexity.
Key Takeaways:
-
V8 isolates enable density that containers can't match, but come with CPU/memory constraints.
-
Cold starts are your enemy—minimize top-level work, lazy-load dependencies, use KV for fast config access.
-
Edge KV is eventually consistent—design for it. Use Durable Objects when you need consistency, but understand the latency tradeoff.
-
Cache hierarchies matter—understand L1 (isolate) → L2 (PoP) → L3 (KV) → L4 (shield) → L5 (origin) and where your data should live.
-
Request coalescing prevents stampedes—never let multiple cache misses become multiple origin requests.
-
Observability is harder at edge—invest in distributed tracing, structured logging, and async metrics export.
-
Edge isn't always faster—when you need origin data anyway, edge compute adds latency. Profile before optimizing.
The edge is a tool, not a destination. Use it where it helps, skip it where it doesn't.
What did you think?