Edge computing moves computation and data storage closer to users, dramatically reducing latency and bandwidth costs. As applications become more real-time and global, edge computing has evolved from a nice-to-have to a necessity. This post explores practical edge computing patterns for modern applications.
What is Edge Computing?
Edge computing runs application logic at the network edge—geographically close to users—rather than in centralized data centers.
Traditional:
User (Tokyo) → 200ms → US Data Center → 200ms → Response
Total: 400ms+ latency
Edge Computing:
User (Tokyo) → 20ms → Tokyo Edge Node → 20ms → Response
Total: 40ms latency
Edge Computing Layers
Modern edge architectures have multiple layers:
User Device (Extreme Edge)
↓
CDN Edge (PoPs worldwide)
↓
Regional Edge (Major cities)
↓
Central Cloud (Data centers)
Layer 1: CDN Edge
Use CDN edge for static content and simple logic:
// Cloudflare Workers example
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const url = new URL(request.url)
// Route based on path
if (url.pathname.startsWith('/api/')) {
return handleAPI(request)
}
// Serve static content from cache
const cache = caches.default
let response = await cache.match(request)
if (!response) {
response = await fetch(request)
// Cache for 1 hour
const cachedResponse = new Response(response.body, response)
cachedResponse.headers.set('Cache-Control', 'max-age=3600')
event.waitUntil(cache.put(request, cachedResponse.clone()))
}
return response
}
async function handleAPI(request) {
// Simple API logic at edge
const url = new URL(request.url)
// A/B testing at edge
const variant = getABVariant(request)
// Geo-based routing
const country = request.headers.get('CF-IPCountry')
const region = getRegion(country)
// Forward to nearest origin
const origin = getOriginForRegion(region)
return fetch(origin + url.pathname, request)
}
function getABVariant(request) {
const cookie = request.headers.get('Cookie')
if (cookie && cookie.includes('variant=')) {
return cookie.match(/variant=(\w+)/)[1]
}
// Assign based on IP hash
const ip = request.headers.get('CF-Connecting-IP')
const hash = hashCode(ip)
return hash % 2 === 0 ? 'A' : 'B'
}
Layer 2: Regional Edge
Deploy full application logic at regional edges:
# FastAPI app deployed to multiple regions
from fastapi import FastAPI, Request
from geopy.distance import geodesic
app = FastAPI()
# Each edge node knows nearby resources
REGIONAL_DATABASES = {
'us-west': 'postgres-us-west.example.com',
'us-east': 'postgres-us-east.example.com',
'eu-west': 'postgres-eu-west.example.com',
'ap-southeast': 'postgres-ap-southeast.example.com',
}
@app.middleware("http")
async def route_to_nearest_db(request: Request, call_next):
"""
Connect to geographically nearest database
"""
# Get user location from headers (set by load balancer)
user_lat = float(request.headers.get('X-User-Lat', 0))
user_lon = float(request.headers.get('X-User-Lon', 0))
# Find nearest region
nearest_region = find_nearest_region(user_lat, user_lon)
# Set database connection for this request
request.state.db_host = REGIONAL_DATABASES[nearest_region]
response = await call_next(request)
response.headers['X-Served-By'] = nearest_region
return response
def find_nearest_region(lat: float, lon: float) -> str:
"""
Find nearest region based on geographic distance
"""
REGION_COORDS = {
'us-west': (37.7749, -122.4194), # San Francisco
'us-east': (40.7128, -74.0060), # New York
'eu-west': (51.5074, -0.1278), # London
'ap-southeast': (1.3521, 103.8198), # Singapore
}
user_location = (lat, lon)
nearest = min(
REGION_COORDS.items(),
key=lambda x: geodesic(user_location, x[1]).kilometers
)
return nearest[0]
@app.get("/api/user/{user_id}")
async def get_user(user_id: str, request: Request):
"""
Query local database replica
"""
db_host = request.state.db_host
db = await connect_to_db(db_host)
user = await db.fetch_one(
"SELECT * FROM users WHERE id = :user_id",
{"user_id": user_id}
)
return user
Edge Data Synchronization
One of the biggest challenges in edge computing is keeping data synchronized across edge nodes.
Pattern 1: Read from Edge, Write to Center
type EdgeCache struct {
localCache *lru.Cache
centralDB *sql.DB
replication *ReplicationClient
}
// Reads are served from local edge cache
func (e *EdgeCache) Get(key string) (interface{}, error) {
// Try local cache first
if val, ok := e.localCache.Get(key); ok {
metrics.RecordCacheHit("edge")
return val, nil
}
metrics.RecordCacheMiss("edge")
// Fallback to central DB
val, err := e.centralDB.Query("SELECT * FROM items WHERE id = ?", key)
if err != nil {
return nil, err
}
// Cache locally
e.localCache.Add(key, val)
return val, nil
}
// Writes go to central, then propagate to edges
func (e *EdgeCache) Set(key string, value interface{}) error {
// Write to central database
_, err := e.centralDB.Exec(
"INSERT INTO items (id, data) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET data = ?",
key, value, value,
)
if err != nil {
return err
}
// Invalidate edge caches
e.replication.InvalidateKey(key)
return nil
}
// Replication stream updates edge caches
func (e *EdgeCache) StartReplication() {
go func() {
for update := range e.replication.Updates() {
switch update.Type {
case "INSERT", "UPDATE":
e.localCache.Add(update.Key, update.Value)
case "DELETE":
e.localCache.Remove(update.Key)
}
}
}()
}
Pattern 2: CRDTs for Multi-Master
For scenarios requiring writes at multiple edges, use CRDTs (Conflict-free Replicated Data Types):
from typing import Dict, Set
import time
class GCounter:
"""
Grow-only counter CRDT
Each edge node has its own counter
"""
def __init__(self, node_id: str):
self.node_id = node_id
self.counts: Dict[str, int] = {node_id: 0}
def increment(self):
"""Increment local counter"""
self.counts[self.node_id] += 1
def merge(self, other: 'GCounter'):
"""Merge with another node's counter"""
for node, count in other.counts.items():
self.counts[node] = max(
self.counts.get(node, 0),
count
)
def value(self) -> int:
"""Get total count across all nodes"""
return sum(self.counts.values())
class ORSet:
"""
Observed-Remove Set CRDT
Handles concurrent adds and removes
"""
def __init__(self, node_id: str):
self.node_id = node_id
self.adds: Dict[str, Set[str]] = {} # element -> set of unique tags
self.removes: Set[str] = set() # set of removed tags
def add(self, element: str):
"""Add element with unique tag"""
tag = f"{self.node_id}-{time.time()}"
if element not in self.adds:
self.adds[element] = set()
self.adds[element].add(tag)
def remove(self, element: str):
"""Remove element by marking all its tags as removed"""
if element in self.adds:
self.removes.update(self.adds[element])
def merge(self, other: 'ORSet'):
"""Merge with another node's set"""
# Merge adds
for element, tags in other.adds.items():
if element not in self.adds:
self.adds[element] = set()
self.adds[element].update(tags)
# Merge removes
self.removes.update(other.removes)
def elements(self) -> Set[str]:
"""Get current set elements"""
result = set()
for element, tags in self.adds.items():
# Element is present if it has tags not in removes
if tags - self.removes:
result.add(element)
return result
# Usage across edge nodes
class DistributedCounter:
def __init__(self, node_id: str, sync_interval: int = 5):
self.counter = GCounter(node_id)
self.node_id = node_id
self.peers = []
def increment(self):
self.counter.increment()
def sync_with_peers(self):
"""Periodically sync with other edge nodes"""
for peer in self.peers:
remote_counter = peer.get_counter()
self.counter.merge(remote_counter)
def get_value(self) -> int:
return self.counter.value()
Edge Computing with WebAssembly
WebAssembly enables running arbitrary code at the edge with near-native performance:
// Rust code compiled to WebAssembly for edge execution
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Request {
user_id: String,
action: String,
}
#[derive(Serialize)]
struct Response {
allowed: bool,
reason: Option<String>,
}
#[wasm_bindgen]
pub fn authorize_request(request_json: &str) -> String {
let request: Request = serde_json::from_str(request_json)
.expect("Invalid JSON");
// Complex authorization logic runs at edge
let allowed = check_rate_limit(&request.user_id)
&& check_permissions(&request.user_id, &request.action)
&& check_geo_restrictions(&request.user_id);
let response = Response {
allowed,
reason: if !allowed {
Some("Access denied".to_string())
} else {
None
},
};
serde_json::to_string(&response).unwrap()
}
fn check_rate_limit(user_id: &str) -> bool {
// Rate limiting logic
// Can access edge KV store
true
}
fn check_permissions(user_id: &str, action: &str) -> bool {
// Permission checking
true
}
fn check_geo_restrictions(user_id: &str) -> bool {
// Geographic restrictions
true
}
Deploy this WASM module to CDN edge:
// Cloudflare Worker loading WASM
import wasm from './authorization.wasm'
const instance = await WebAssembly.instantiate(wasm)
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const requestData = await request.json()
// Call WASM function at edge
const result = instance.exports.authorize_request(
JSON.stringify(requestData)
)
const response = JSON.parse(result)
if (response.allowed) {
// Forward to origin
return fetch(ORIGIN_URL, request)
} else {
// Deny at edge
return new Response(response.reason, { status: 403 })
}
}
Edge Computing for Real-Time Analytics
Process analytics at the edge to reduce data transfer:
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict
import asyncio
@dataclass
class Event:
user_id: str
event_type: str
timestamp: float
properties: Dict
class EdgeAnalytics:
"""
Aggregate analytics at edge, sync summaries to central
"""
def __init__(self, edge_id: str):
self.edge_id = edge_id
self.local_counts = defaultdict(int)
self.central_sync_interval = 60 # seconds
async def track_event(self, event: Event):
"""
Process event at edge
"""
# Increment local counters
key = f"{event.event_type}:{event.user_id}"
self.local_counts[key] += 1
# Store locally for real-time queries
await self.store_local(event)
# Don't send individual events to central
# Sync aggregates periodically instead
async def store_local(self, event: Event):
"""
Store in edge database for local queries
"""
# Local SQLite/Redis
await self.local_db.execute(
"INSERT INTO events (user_id, type, timestamp) VALUES (?, ?, ?)",
event.user_id, event.event_type, event.timestamp
)
async def sync_to_central(self):
"""
Periodically sync aggregates to central
Reduces bandwidth by 100x compared to raw events
"""
while True:
await asyncio.sleep(self.central_sync_interval)
if not self.local_counts:
continue
# Send aggregated counts instead of raw events
summary = {
'edge_id': self.edge_id,
'timestamp': time.time(),
'counts': dict(self.local_counts),
'total_events': sum(self.local_counts.values())
}
await self.send_to_central(summary)
# Reset local counts
self.local_counts.clear()
async def send_to_central(self, summary: Dict):
"""
Send compressed summary to central analytics
"""
# Much smaller payload than raw events
await self.central_client.post('/analytics/summary', json=summary)
async def query_local(self, user_id: str, minutes: int = 60):
"""
Real-time queries served from edge
"""
cutoff = time.time() - (minutes * 60)
events = await self.local_db.fetch_all(
"SELECT * FROM events WHERE user_id = ? AND timestamp > ?",
user_id, cutoff
)
return events
Edge Caching Strategies
Smart caching at the edge dramatically improves performance:
// Intelligent edge caching with stale-while-revalidate
class EdgeCache {
constructor() {
this.cache = caches.default
}
async fetch(request) {
const cacheKey = new Request(request.url, request)
const cached = await this.cache.match(cacheKey)
// Stale-while-revalidate pattern
if (cached) {
const age = this.getAge(cached)
const maxAge = this.getMaxAge(cached)
if (age < maxAge) {
// Fresh cache hit
return cached
} else if (age < maxAge * 2) {
// Stale but acceptable
// Return stale, refresh in background
this.refreshInBackground(request, cacheKey)
return cached
}
}
// Cache miss or too stale
return this.fetchAndCache(request, cacheKey)
}
async fetchAndCache(request, cacheKey) {
const response = await fetch(request)
// Cache successful responses
if (response.ok) {
const clonedResponse = response.clone()
await this.cache.put(cacheKey, clonedResponse)
}
return response
}
refreshInBackground(request, cacheKey) {
// Don't await - refresh in background
this.fetchAndCache(request, cacheKey).catch(err => {
console.error('Background refresh failed:', err)
})
}
getAge(response) {
const dateHeader = response.headers.get('Date')
const date = new Date(dateHeader)
return (Date.now() - date.getTime()) / 1000
}
getMaxAge(response) {
const cacheControl = response.headers.get('Cache-Control')
const match = cacheControl?.match(/max-age=(\d+)/)
return match ? parseInt(match[1]) : 0
}
}
Edge Function Deployment
Deploy serverless functions to edge locations:
# Deploying to Cloudflare Workers (300+ edge locations)
# wrangler.toml
name = "edge-api"
type = "javascript"
account_id = "your-account-id"
workers_dev = true
route = "api.example.com/*"
zone_id = "your-zone-id"
[env.production]
routes = [
{ pattern = "api.example.com/*", zone_id = "your-zone-id" }
]
kv_namespaces = [
{ binding = "CACHE", id = "your-kv-namespace-id" }
]
// Edge function
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const url = new URL(request.url)
// Routing based on path
const routes = {
'/api/user': handleUser,
'/api/recommendations': handleRecommendations,
'/api/search': handleSearch,
}
const handler = routes[url.pathname] || handleNotFound
try {
return await handler(request)
} catch (error) {
return new Response(error.message, { status: 500 })
}
}
async function handleUser(request) {
const userId = new URL(request.url).searchParams.get('id')
// Check KV cache (edge storage)
const cached = await CACHE.get(`user:${userId}`)
if (cached) {
return new Response(cached, {
headers: { 'Content-Type': 'application/json' }
})
}
// Fetch from origin
const response = await fetch(`${ORIGIN}/users/${userId}`)
const data = await response.text()
// Cache at edge
await CACHE.put(`user:${userId}`, data, {
expirationTtl: 3600
})
return new Response(data, {
headers: { 'Content-Type': 'application/json' }
})
}
Monitoring Edge Deployments
class EdgeMonitoring:
"""
Monitor health and performance across edge nodes
"""
def __init__(self):
self.metrics = MetricsCollector()
def record_request(self, edge_id: str, latency_ms: float,
cache_hit: bool):
"""
Record metrics per edge location
"""
self.metrics.histogram(
'edge.request.latency',
latency_ms,
tags={'edge_id': edge_id, 'cache_hit': cache_hit}
)
def check_edge_health(self, edge_id: str) -> bool:
"""
Health check for edge node
"""
# Check recent error rate
error_rate = self.metrics.get_rate(
'edge.errors',
tags={'edge_id': edge_id},
window_seconds=300
)
# Check latency
p99_latency = self.metrics.get_percentile(
'edge.request.latency',
0.99,
tags={'edge_id': edge_id},
window_seconds=300
)
is_healthy = error_rate < 0.01 and p99_latency < 200
if not is_healthy:
self.alert_unhealthy_edge(edge_id, error_rate, p99_latency)
return is_healthy
def get_edge_statistics(self) -> Dict:
"""
Global statistics across all edges
"""
return {
'total_requests': self.metrics.get_count('edge.requests'),
'avg_latency': self.metrics.get_mean('edge.request.latency'),
'cache_hit_rate': self.calculate_cache_hit_rate(),
'edge_count': len(self.get_active_edges()),
'unhealthy_edges': self.get_unhealthy_edges()
}
Key Takeaways
- Use edge for latency-sensitive operations: Static content, simple APIs, authorization
- Keep edge logic simple: Complex processing belongs in regional or central
- Plan for eventual consistency: Edge data will be stale
- Monitor per-edge performance: Different regions have different characteristics
- Use CRDTs for multi-master writes: Avoid conflict resolution headaches
- Cache aggressively: Edge’s main value is avoiding origin trips
- Aggregate before syncing: Don’t send raw data from edge to central
Edge computing is becoming essential for global applications. Start with CDN caching, graduate to edge functions, and add regional compute as needed.