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

  1. Use edge for latency-sensitive operations: Static content, simple APIs, authorization
  2. Keep edge logic simple: Complex processing belongs in regional or central
  3. Plan for eventual consistency: Edge data will be stale
  4. Monitor per-edge performance: Different regions have different characteristics
  5. Use CRDTs for multi-master writes: Avoid conflict resolution headaches
  6. Cache aggressively: Edge’s main value is avoiding origin trips
  7. 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.