Back to Blog

Multi-Region Architecture for Modern Web Apps

Edge compute, data consistency, replication trade-offs. When your users span continents and your database can only live in one place, every architectural decision becomes a physics problem.


The Speed of Light Problem

Network latency is bounded by physics. Light travels at ~200km/ms in fiber optic cable:

┌─────────────────────────────────────────────────────────────────┐
│                 Global Round-Trip Latencies                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Origin: us-east-1 (Virginia)                                   │
│                                                                  │
│  ┌────────────────────┬──────────┬──────────────────────┐       │
│  │ Destination        │ Distance │ Min RTT (theoretical)│       │
│  ├────────────────────┼──────────┼──────────────────────┤       │
│  │ New York           │ 400 km   │ 4ms                  │       │
│  │ San Francisco      │ 4,100 km │ 41ms                 │       │
│  │ London             │ 5,600 km │ 56ms                 │       │
│  │ Frankfurt          │ 6,600 km │ 66ms                 │       │
│  │ Singapore          │ 15,300km │ 153ms                │       │
│  │ Sydney             │ 15,900km │ 159ms                │       │
│  │ Mumbai             │ 13,000km │ 130ms                │       │
│  │ São Paulo          │ 7,700 km │ 77ms                 │       │
│  └────────────────────┴──────────┴──────────────────────┘       │
│                                                                  │
│  Reality adds 2-3x overhead:                                    │
│  • Routing inefficiency                                         │
│  • Network hops                                                 │
│  • Processing at each hop                                       │
│  • Protocol overhead                                            │
│                                                                  │
│  Actual RTT Singapore → Virginia: ~250-350ms                    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

For a user in Singapore accessing a US-based application:

┌─────────────────────────────────────────────────────────────────┐
│             Single-Region Request Breakdown                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  User in Singapore → Server in Virginia                         │
│                                                                  │
│  DNS lookup:           50ms                                     │
│  TCP handshake:        250ms (1 RTT)                            │
│  TLS handshake:        500ms (2 RTT)                            │
│  HTTP request:         250ms (1 RTT)                            │
│  Server processing:    100ms                                    │
│  HTTP response:        250ms (1 RTT)                            │
│  ─────────────────────────────                                  │
│  Total TTFB:           1400ms                                   │
│                                                                  │
│  + Asset downloads:    500-2000ms                               │
│  + JS execution:       200-500ms                                │
│  ─────────────────────────────                                  │
│  Total TTI:            2100-3900ms                              │
│                                                                  │
│  This is unacceptable for modern applications.                  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Multi-Region Architecture Patterns

Pattern 1: Edge-Accelerated Single Origin

┌─────────────────────────────────────────────────────────────────┐
│                Edge-Accelerated Single Origin                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│                      ┌─────────────┐                            │
│                      │   Origin    │                            │
│                      │  (us-east)  │                            │
│                      │  Database   │                            │
│                      └──────┬──────┘                            │
│                             │                                    │
│              ┌──────────────┼──────────────┐                    │
│              │              │              │                    │
│         ┌────▼────┐    ┌────▼────┐   ┌────▼────┐               │
│         │  Edge   │    │  Edge   │   │  Edge   │               │
│         │ (Asia)  │    │ (Europe)│   │ (LATAM) │               │
│         └────┬────┘    └────┬────┘   └────┬────┘               │
│              │              │              │                    │
│         ┌────▼────┐    ┌────▼────┐   ┌────▼────┐               │
│         │  Users  │    │  Users  │   │  Users  │               │
│         └─────────┘    └─────────┘   └─────────┘               │
│                                                                  │
│  Edge handles:                                                  │
│  • Static assets (cached)                                       │
│  • TLS termination                                              │
│  • Compression                                                  │
│  • Rate limiting                                                │
│  • Bot protection                                               │
│                                                                  │
│  Origin handles:                                                │
│  • Dynamic content                                              │
│  • Database queries                                             │
│  • Business logic                                               │
│                                                                  │
│  Pros: Simple, single source of truth                           │
│  Cons: Dynamic requests still hit origin                        │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Pattern 2: Edge Compute with Origin Fallback

┌─────────────────────────────────────────────────────────────────┐
│              Edge Compute with Origin Fallback                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  User Request                                                   │
│       │                                                          │
│       ▼                                                          │
│  ┌─────────────┐                                                │
│  │ Edge (SIN)  │◄─── Cloudflare/Vercel/Fastly                   │
│  │             │                                                │
│  │ 1. Auth     │                                                │
│  │ 2. Cache?   │──── HIT ───► Return cached                     │
│  │ 3. Compute? │──── Yes ───► Execute at edge                   │
│  │ 4. Origin   │──── No ────► Fetch from origin                 │
│  │             │                                                │
│  └──────┬──────┘                                                │
│         │                                                        │
│         │ (only uncached dynamic)                                │
│         ▼                                                        │
│  ┌─────────────┐                                                │
│  │   Origin    │                                                │
│  │  (us-east)  │                                                │
│  └─────────────┘                                                │
│                                                                  │
│  Edge computes:                                                 │
│  • Personalization (geo, A/B tests)                             │
│  • Auth validation                                              │
│  • API routing                                                  │
│  • Rate limiting                                                │
│  • Header manipulation                                          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Pattern 3: Multi-Region Active-Active

┌─────────────────────────────────────────────────────────────────┐
│                Multi-Region Active-Active                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                   Global Load Balancer                   │   │
│  │              (Anycast DNS / GeoDNS)                      │   │
│  └─────────────────────────┬───────────────────────────────┘   │
│                            │                                    │
│           ┌────────────────┼────────────────┐                  │
│           │                │                │                   │
│           ▼                ▼                ▼                   │
│    ┌────────────┐   ┌────────────┐   ┌────────────┐            │
│    │  Region A  │   │  Region B  │   │  Region C  │            │
│    │  us-east   │   │  eu-west   │   │  ap-south  │            │
│    │            │   │            │   │            │            │
│    │  ┌──────┐  │   │  ┌──────┐  │   │  ┌──────┐  │            │
│    │  │ App  │  │   │  │ App  │  │   │  │ App  │  │            │
│    │  └───┬──┘  │   │  └───┬──┘  │   │  └───┬──┘  │            │
│    │      │     │   │      │     │   │      │     │            │
│    │  ┌───▼──┐  │   │  ┌───▼──┐  │   │  ┌───▼──┐  │            │
│    │  │  DB  │◄─┼───┼──┤  DB  │◄─┼───┼──┤  DB  │  │            │
│    │  │replica│  │   │  │replica│  │   │  │replica│  │          │
│    │  └──────┘  │   │  └──────┘  │   │  └──────┘  │            │
│    └────────────┘   └────────────┘   └────────────┘            │
│           │                │                │                   │
│           └────────────────┼────────────────┘                  │
│                            │                                    │
│                     ┌──────▼──────┐                            │
│                     │   Primary   │                            │
│                     │   Database  │                            │
│                     │  (us-east)  │                            │
│                     └─────────────┘                            │
│                                                                  │
│  Replication: Async (eventual consistency)                      │
│  Writes: Routed to primary or use conflict resolution           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Data Consistency Strategies

CAP Theorem in Practice

┌─────────────────────────────────────────────────────────────────┐
│                    CAP Theorem Trade-offs                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  You can only have 2 of 3:                                      │
│                                                                  │
│        Consistency                                               │
│            /\                                                    │
│           /  \                                                   │
│          /    \                                                  │
│         / CP   \  CA                                            │
│        /        \                                                │
│       /    AP    \                                               │
│      /____________\                                              │
│  Availability    Partition                                       │
│                  Tolerance                                       │
│                                                                  │
│  CP Systems (Consistency + Partition Tolerance):                │
│  • PostgreSQL single-region                                     │
│  • CockroachDB                                                  │
│  • Spanner                                                      │
│  Trade-off: Reject writes during partitions                     │
│                                                                  │
│  AP Systems (Availability + Partition Tolerance):               │
│  • Cassandra                                                    │
│  • DynamoDB (eventual consistency mode)                         │
│  • CockroachDB (with stale reads)                               │
│  Trade-off: May return stale data                               │
│                                                                  │
│  CA Systems (Consistency + Availability):                       │
│  • Single-node databases                                        │
│  Trade-off: No partition tolerance (single point of failure)    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Consistency Models by Use Case

// Different consistency requirements for different data

interface ConsistencyConfig {
  model: 'strong' | 'eventual' | 'causal' | 'session';
  maxStaleness?: number; // milliseconds
  readFromReplica: boolean;
  writeToRegion: 'local' | 'primary' | 'quorum';
}

const CONSISTENCY_PROFILES: Record<string, ConsistencyConfig> = {
  // Financial transactions - strong consistency
  payments: {
    model: 'strong',
    readFromReplica: false,
    writeToRegion: 'primary',
  },

  // User authentication - session consistency
  auth: {
    model: 'session',
    readFromReplica: true,
    writeToRegion: 'primary',
  },

  // Product catalog - eventual consistency OK
  products: {
    model: 'eventual',
    maxStaleness: 60000, // 1 minute stale is fine
    readFromReplica: true,
    writeToRegion: 'primary',
  },

  // User preferences - causal consistency
  preferences: {
    model: 'causal',
    readFromReplica: true,
    writeToRegion: 'local',
  },

  // Analytics/metrics - highly eventual
  analytics: {
    model: 'eventual',
    maxStaleness: 300000, // 5 minutes
    readFromReplica: true,
    writeToRegion: 'local',
  },
};

Read-Your-Writes Consistency

// Ensure users see their own writes immediately

class SessionConsistency {
  private lastWriteTimestamps: Map<string, number> = new Map();

  async write(
    userId: string,
    entity: string,
    data: unknown
  ): Promise<WriteResult> {
    const result = await this.primaryDB.write(entity, data);

    // Track write timestamp
    const key = `${userId}:${entity}`;
    this.lastWriteTimestamps.set(key, result.timestamp);

    return result;
  }

  async read(
    userId: string,
    entity: string,
    query: Query
  ): Promise<ReadResult> {
    const key = `${userId}:${entity}`;
    const lastWrite = this.lastWriteTimestamps.get(key);

    if (lastWrite) {
      // Check if replica has caught up
      const replicaTimestamp = await this.getReplicaTimestamp();

      if (replicaTimestamp >= lastWrite) {
        // Replica is up to date, safe to read
        return this.replicaDB.read(entity, query);
      } else {
        // Replica is behind, read from primary
        return this.primaryDB.read(entity, query);
      }
    }

    // No recent writes, read from nearest replica
    return this.replicaDB.read(entity, query);
  }
}

// HTTP header-based approach
export function readYourWritesMiddleware(req: Request, res: Response, next: NextFunction) {
  // Client sends timestamp of last write
  const lastWrite = req.headers['x-last-write-timestamp'];

  if (lastWrite) {
    const timestamp = parseInt(lastWrite, 10);

    // Check if we should read from primary
    req.readFromPrimary = shouldReadFromPrimary(timestamp);
  }

  next();
}

// Client-side tracking
class ApiClient {
  private lastWriteTimestamp: number = 0;

  async post(url: string, data: unknown): Promise<Response> {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    });

    // Track write timestamp from response
    const timestamp = response.headers.get('x-write-timestamp');
    if (timestamp) {
      this.lastWriteTimestamp = parseInt(timestamp, 10);
    }

    return response;
  }

  async get(url: string): Promise<Response> {
    return fetch(url, {
      headers: {
        // Send last write timestamp
        'X-Last-Write-Timestamp': String(this.lastWriteTimestamp),
      },
    });
  }
}

Edge Compute Implementation

Vercel Edge Functions

// middleware.ts - Runs at the edge

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Get user's region from Vercel's geo headers
  const country = request.geo?.country ?? 'US';
  const region = request.geo?.region ?? '';
  const city = request.geo?.city ?? '';

  // Add region headers for personalization
  response.headers.set('x-user-country', country);
  response.headers.set('x-user-region', region);

  // Route to nearest origin for specific paths
  if (request.nextUrl.pathname.startsWith('/api/')) {
    const nearestOrigin = getNearestOrigin(country);
    response.headers.set('x-origin-region', nearestOrigin);
  }

  // A/B testing at the edge
  const bucket = getABTestBucket(request);
  response.headers.set('x-ab-bucket', bucket);

  // Feature flags based on region
  const features = getRegionalFeatures(country);
  response.headers.set('x-features', JSON.stringify(features));

  return response;
}

function getNearestOrigin(country: string): string {
  const regionMap: Record<string, string> = {
    // Americas
    US: 'us-east-1',
    CA: 'us-east-1',
    BR: 'sa-east-1',
    MX: 'us-east-1',

    // Europe
    GB: 'eu-west-1',
    DE: 'eu-central-1',
    FR: 'eu-west-1',

    // Asia Pacific
    JP: 'ap-northeast-1',
    SG: 'ap-southeast-1',
    IN: 'ap-south-1',
    AU: 'ap-southeast-2',
  };

  return regionMap[country] ?? 'us-east-1';
}

function getABTestBucket(request: NextRequest): string {
  // Use consistent hashing for sticky sessions
  const userId = request.cookies.get('userId')?.value;
  if (userId) {
    const hash = simpleHash(userId);
    return hash % 2 === 0 ? 'control' : 'variant';
  }

  // Random assignment for new users
  return Math.random() < 0.5 ? 'control' : 'variant';
}

function getRegionalFeatures(country: string): Record<string, boolean> {
  // Different feature availability by region
  return {
    // Payment methods
    applePay: ['US', 'CA', 'GB', 'AU'].includes(country),
    pix: country === 'BR',
    upi: country === 'IN',

    // Regional features
    gdprBanner: isEUCountry(country),
    cookieConsent: isEUCountry(country),

    // Performance features
    aggressivePrefetch: !isSlowRegion(country),
    imageOptimization: true,
  };
}

Cloudflare Workers

// worker.ts - Cloudflare Worker for edge compute

export interface Env {
  DB: D1Database;
  KV: KVNamespace;
  CACHE: Cache;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    // Static assets - cache forever
    if (url.pathname.startsWith('/static/')) {
      return handleStaticAsset(request, env);
    }

    // API routes - edge processing
    if (url.pathname.startsWith('/api/')) {
      return handleAPI(request, env, ctx);
    }

    // Pages - edge SSR with caching
    return handlePage(request, env, ctx);
  },
};

async function handleAPI(
  request: Request,
  env: Env,
  ctx: ExecutionContext
): Promise<Response> {
  const url = new URL(request.url);

  // Edge-cacheable API responses
  if (request.method === 'GET') {
    const cacheKey = new Request(url.toString(), request);
    const cache = caches.default;

    // Check cache
    let response = await cache.match(cacheKey);
    if (response) {
      return response;
    }

    // Fetch from origin
    response = await fetchFromOrigin(request, env);

    // Cache if cacheable
    if (isCacheable(response)) {
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
    }

    return response;
  }

  // Mutations - proxy to origin
  return fetchFromOrigin(request, env);
}

async function handlePage(
  request: Request,
  env: Env,
  ctx: ExecutionContext
): Promise<Response> {
  const url = new URL(request.url);

  // Try edge cache first
  const cacheKey = new Request(url.toString(), request);
  const cache = caches.default;

  let response = await cache.match(cacheKey);
  if (response) {
    // Add stale-while-revalidate
    ctx.waitUntil(revalidateInBackground(request, env, cache, cacheKey));
    return response;
  }

  // Edge render for personalized pages
  if (needsPersonalization(request)) {
    return edgeRender(request, env);
  }

  // Fetch from origin
  response = await fetchFromOrigin(request, env);

  // Cache non-personalized pages
  if (!needsPersonalization(request)) {
    ctx.waitUntil(cache.put(cacheKey, response.clone()));
  }

  return response;
}

async function edgeRender(request: Request, env: Env): Promise<Response> {
  // Get user context
  const country = request.cf?.country ?? 'US';
  const city = request.cf?.city ?? '';

  // Fetch data from edge KV
  const config = await env.KV.get(`config:${country}`, 'json');

  // Render HTML at edge
  const html = renderPage({
    country,
    city,
    config,
    timestamp: Date.now(),
  });

  return new Response(html, {
    headers: {
      'Content-Type': 'text/html',
      'Cache-Control': 'private, max-age=0',
    },
  });
}

function needsPersonalization(request: Request): boolean {
  // Check for auth cookies
  const cookies = request.headers.get('cookie');
  if (cookies?.includes('auth=')) return true;

  // Check for personalization params
  const url = new URL(request.url);
  if (url.searchParams.has('ref')) return true;

  return false;
}

Database Replication Patterns

PostgreSQL with Read Replicas

// database-router.ts

import { Pool } from 'pg';

interface DatabaseConfig {
  primary: Pool;
  replicas: Pool[];
}

class DatabaseRouter {
  private primary: Pool;
  private replicas: Pool[];
  private replicaIndex = 0;

  constructor(config: DatabaseConfig) {
    this.primary = config.primary;
    this.replicas = config.replicas;
  }

  /**
   * Get appropriate connection based on query type
   */
  getConnection(options: {
    write: boolean;
    consistency?: 'strong' | 'eventual';
    region?: string;
  }): Pool {
    // Writes always go to primary
    if (options.write) {
      return this.primary;
    }

    // Strong consistency reads go to primary
    if (options.consistency === 'strong') {
      return this.primary;
    }

    // Route to nearest replica
    if (options.region) {
      const regionalReplica = this.getRegionalReplica(options.region);
      if (regionalReplica) return regionalReplica;
    }

    // Round-robin across replicas
    return this.getNextReplica();
  }

  private getRegionalReplica(region: string): Pool | null {
    // Map regions to replica indices
    const regionMap: Record<string, number> = {
      'us-east': 0,
      'eu-west': 1,
      'ap-south': 2,
    };

    const index = regionMap[region];
    if (index !== undefined && this.replicas[index]) {
      return this.replicas[index];
    }

    return null;
  }

  private getNextReplica(): Pool {
    const replica = this.replicas[this.replicaIndex];
    this.replicaIndex = (this.replicaIndex + 1) % this.replicas.length;
    return replica;
  }

  /**
   * Execute query with automatic routing
   */
  async query<T>(
    sql: string,
    params: unknown[],
    options: QueryOptions = {}
  ): Promise<T[]> {
    const isWrite = this.isWriteQuery(sql);
    const pool = this.getConnection({
      write: isWrite,
      consistency: options.consistency,
      region: options.region,
    });

    const start = performance.now();
    const result = await pool.query(sql, params);
    const duration = performance.now() - start;

    // Log routing decision
    console.log({
      type: isWrite ? 'write' : 'read',
      pool: isWrite ? 'primary' : 'replica',
      region: options.region,
      duration,
    });

    return result.rows as T[];
  }

  private isWriteQuery(sql: string): boolean {
    const writePatterns = /^(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)/i;
    return writePatterns.test(sql.trim());
  }
}

interface QueryOptions {
  consistency?: 'strong' | 'eventual';
  region?: string;
}

CockroachDB Multi-Region

-- CockroachDB multi-region configuration

-- Create database with multi-region config
CREATE DATABASE app
  PRIMARY REGION "us-east1"
  REGIONS "us-east1", "europe-west1", "asia-southeast1"
  SURVIVE REGION FAILURE;

-- Global tables (replicated everywhere, strong consistency)
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email STRING NOT NULL UNIQUE,
  created_at TIMESTAMPTZ DEFAULT now()
) LOCALITY GLOBAL;

-- Regional tables (data stays in region)
CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  region STRING NOT NULL,
  total DECIMAL(10,2),
  created_at TIMESTAMPTZ DEFAULT now()
) LOCALITY REGIONAL BY ROW AS region;

-- Regional by table (entire table in one region)
CREATE TABLE audit_logs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_type STRING,
  data JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
) LOCALITY REGIONAL BY TABLE IN "us-east1";
// CockroachDB client with region awareness

import { Client } from 'pg';

class CockroachClient {
  private clients: Map<string, Client> = new Map();

  async getRegionalClient(region: string): Promise<Client> {
    if (!this.clients.has(region)) {
      const client = new Client({
        connectionString: this.getConnectionString(region),
      });
      await client.connect();
      this.clients.set(region, client);
    }

    return this.clients.get(region)!;
  }

  private getConnectionString(region: string): string {
    // CockroachDB Serverless provides regional endpoints
    const endpoints: Record<string, string> = {
      'us-east1': 'postgresql://user@cluster.us-east1.cockroachlabs.cloud:26257/app',
      'europe-west1': 'postgresql://user@cluster.europe-west1.cockroachlabs.cloud:26257/app',
      'asia-southeast1': 'postgresql://user@cluster.asia-southeast1.cockroachlabs.cloud:26257/app',
    };

    return endpoints[region] ?? endpoints['us-east1'];
  }

  async query<T>(
    sql: string,
    params: unknown[],
    options: { region: string; staleRead?: boolean }
  ): Promise<T[]> {
    const client = await this.getRegionalClient(options.region);

    // Use follower reads for stale-tolerant queries
    if (options.staleRead) {
      await client.query("SET TRANSACTION AS OF SYSTEM TIME '-10s'");
    }

    const result = await client.query(sql, params);
    return result.rows as T[];
  }
}

Traffic Routing Strategies

Geo-Based DNS Routing

// Infrastructure as Code - Terraform

/*
resource "aws_route53_health_check" "primary" {
  fqdn              = "us-east.api.example.com"
  port              = 443
  type              = "HTTPS"
  resource_path     = "/health"
  failure_threshold = 3
  request_interval  = 10
}

resource "aws_route53_record" "api_geo" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "api.example.com"
  type    = "A"

  set_identifier = "us-east"
  geolocation_routing_policy {
    continent = "NA"
  }

  alias {
    name                   = aws_lb.us_east.dns_name
    zone_id                = aws_lb.us_east.zone_id
    evaluate_target_health = true
  }

  health_check_id = aws_route53_health_check.primary.id
}

resource "aws_route53_record" "api_geo_eu" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "api.example.com"
  type    = "A"

  set_identifier = "eu-west"
  geolocation_routing_policy {
    continent = "EU"
  }

  alias {
    name                   = aws_lb.eu_west.dns_name
    zone_id                = aws_lb.eu_west.zone_id
    evaluate_target_health = true
  }
}
*/

// Application-level routing
export function routeToRegion(request: Request): string {
  // Get user's region from headers (set by CDN)
  const country = request.headers.get('cf-ipcountry') ?? 'US';

  // Map countries to API regions
  const regionMap: Record<string, string> = {
    // North America
    US: 'us-east',
    CA: 'us-east',
    MX: 'us-east',

    // Europe
    GB: 'eu-west',
    DE: 'eu-west',
    FR: 'eu-west',
    // ... more EU countries

    // Asia Pacific
    JP: 'ap-northeast',
    KR: 'ap-northeast',
    SG: 'ap-southeast',
    IN: 'ap-south',
    AU: 'ap-southeast',
  };

  return regionMap[country] ?? 'us-east';
}

Latency-Based Routing

// Client-side latency measurement for routing

class LatencyBasedRouter {
  private latencies: Map<string, number[]> = new Map();
  private endpoints = [
    { region: 'us-east', url: 'https://us-east.api.example.com' },
    { region: 'eu-west', url: 'https://eu-west.api.example.com' },
    { region: 'ap-south', url: 'https://ap-south.api.example.com' },
  ];

  async measureLatencies(): Promise<void> {
    const measurements = await Promise.all(
      this.endpoints.map(async (endpoint) => {
        const start = performance.now();

        try {
          await fetch(`${endpoint.url}/ping`, {
            method: 'HEAD',
            cache: 'no-store',
          });
          const latency = performance.now() - start;

          return { region: endpoint.region, latency };
        } catch {
          return { region: endpoint.region, latency: Infinity };
        }
      })
    );

    // Store measurements
    measurements.forEach(({ region, latency }) => {
      if (!this.latencies.has(region)) {
        this.latencies.set(region, []);
      }
      const history = this.latencies.get(region)!;
      history.push(latency);

      // Keep last 10 measurements
      if (history.length > 10) {
        history.shift();
      }
    });
  }

  getBestRegion(): string {
    let bestRegion = 'us-east';
    let bestLatency = Infinity;

    this.latencies.forEach((history, region) => {
      // Use median for stability
      const sorted = [...history].sort((a, b) => a - b);
      const median = sorted[Math.floor(sorted.length / 2)];

      if (median < bestLatency) {
        bestLatency = median;
        bestRegion = region;
      }
    });

    return bestRegion;
  }

  getEndpoint(): string {
    const region = this.getBestRegion();
    return this.endpoints.find((e) => e.region === region)!.url;
  }
}

// Usage
const router = new LatencyBasedRouter();

// Measure on app start
router.measureLatencies();

// Re-measure periodically
setInterval(() => router.measureLatencies(), 60000);

// Use best endpoint
const apiUrl = router.getEndpoint();

Conflict Resolution

Last-Write-Wins (LWW)

// Simple but lossy conflict resolution

interface VersionedData<T> {
  data: T;
  version: number;
  timestamp: number;
  region: string;
}

class LWWConflictResolver<T> {
  resolve(
    local: VersionedData<T>,
    remote: VersionedData<T>
  ): VersionedData<T> {
    // Higher timestamp wins
    if (remote.timestamp > local.timestamp) {
      return remote;
    }

    // Tie-breaker: higher region name (deterministic)
    if (remote.timestamp === local.timestamp) {
      return remote.region > local.region ? remote : local;
    }

    return local;
  }
}

CRDTs (Conflict-free Replicated Data Types)

// G-Counter CRDT for distributed counters

class GCounter {
  private counts: Map<string, number> = new Map();

  constructor(private nodeId: string) {}

  increment(amount: number = 1): void {
    const current = this.counts.get(this.nodeId) ?? 0;
    this.counts.set(this.nodeId, current + amount);
  }

  value(): number {
    let total = 0;
    this.counts.forEach((count) => {
      total += count;
    });
    return total;
  }

  merge(other: GCounter): void {
    other.counts.forEach((count, nodeId) => {
      const current = this.counts.get(nodeId) ?? 0;
      this.counts.set(nodeId, Math.max(current, count));
    });
  }

  toJSON(): Record<string, number> {
    return Object.fromEntries(this.counts);
  }

  static fromJSON(nodeId: string, data: Record<string, number>): GCounter {
    const counter = new GCounter(nodeId);
    Object.entries(data).forEach(([node, count]) => {
      counter.counts.set(node, count);
    });
    return counter;
  }
}

// LWW-Register CRDT for single values
class LWWRegister<T> {
  constructor(
    private value: T,
    private timestamp: number,
    private nodeId: string
  ) {}

  set(value: T, timestamp: number = Date.now()): void {
    if (timestamp > this.timestamp) {
      this.value = value;
      this.timestamp = timestamp;
    }
  }

  get(): T {
    return this.value;
  }

  merge(other: LWWRegister<T>): void {
    if (other.timestamp > this.timestamp) {
      this.value = other.value;
      this.timestamp = other.timestamp;
    } else if (other.timestamp === this.timestamp) {
      // Tie-breaker
      if (other.nodeId > this.nodeId) {
        this.value = other.value;
      }
    }
  }
}

// OR-Set CRDT for sets with add/remove
class ORSet<T> {
  private elements: Map<T, Set<string>> = new Map(); // value -> set of unique tags
  private tombstones: Map<T, Set<string>> = new Map(); // removed tags

  constructor(private nodeId: string) {}

  add(element: T): void {
    const tag = `${this.nodeId}:${Date.now()}:${Math.random()}`;

    if (!this.elements.has(element)) {
      this.elements.set(element, new Set());
    }
    this.elements.get(element)!.add(tag);
  }

  remove(element: T): void {
    const tags = this.elements.get(element);
    if (tags) {
      if (!this.tombstones.has(element)) {
        this.tombstones.set(element, new Set());
      }
      tags.forEach((tag) => {
        this.tombstones.get(element)!.add(tag);
      });
      this.elements.delete(element);
    }
  }

  has(element: T): boolean {
    const tags = this.elements.get(element);
    const tombs = this.tombstones.get(element) ?? new Set();

    if (!tags) return false;

    // Check if any tag is not tombstoned
    for (const tag of tags) {
      if (!tombs.has(tag)) return true;
    }
    return false;
  }

  values(): T[] {
    return Array.from(this.elements.keys()).filter((e) => this.has(e));
  }

  merge(other: ORSet<T>): void {
    // Merge elements
    other.elements.forEach((tags, element) => {
      if (!this.elements.has(element)) {
        this.elements.set(element, new Set());
      }
      tags.forEach((tag) => {
        this.elements.get(element)!.add(tag);
      });
    });

    // Merge tombstones
    other.tombstones.forEach((tags, element) => {
      if (!this.tombstones.has(element)) {
        this.tombstones.set(element, new Set());
      }
      tags.forEach((tag) => {
        this.tombstones.get(element)!.add(tag);
      });
    });
  }
}

Monitoring Multi-Region Systems

// Multi-region health dashboard

interface RegionHealth {
  region: string;
  status: 'healthy' | 'degraded' | 'unhealthy';
  latency: {
    p50: number;
    p95: number;
    p99: number;
  };
  errorRate: number;
  replicationLag: number;
  lastCheck: number;
}

class MultiRegionMonitor {
  private health: Map<string, RegionHealth> = new Map();

  async checkAllRegions(): Promise<void> {
    const regions = ['us-east', 'eu-west', 'ap-south'];

    const checks = await Promise.all(
      regions.map((region) => this.checkRegion(region))
    );

    checks.forEach((check) => {
      this.health.set(check.region, check);
    });
  }

  private async checkRegion(region: string): Promise<RegionHealth> {
    const endpoint = `https://${region}.api.example.com`;

    // Latency check
    const latencies: number[] = [];
    for (let i = 0; i < 10; i++) {
      const start = performance.now();
      await fetch(`${endpoint}/health`);
      latencies.push(performance.now() - start);
    }

    latencies.sort((a, b) => a - b);

    // Error rate (last 5 minutes)
    const errorRate = await this.getErrorRate(region);

    // Replication lag
    const replicationLag = await this.getReplicationLag(region);

    // Determine status
    let status: RegionHealth['status'] = 'healthy';
    if (errorRate > 0.05 || latencies[9] > 500 || replicationLag > 10000) {
      status = 'degraded';
    }
    if (errorRate > 0.1 || latencies[9] > 1000 || replicationLag > 60000) {
      status = 'unhealthy';
    }

    return {
      region,
      status,
      latency: {
        p50: latencies[4],
        p95: latencies[8],
        p99: latencies[9],
      },
      errorRate,
      replicationLag,
      lastCheck: Date.now(),
    };
  }

  private async getErrorRate(region: string): Promise<number> {
    // Query metrics store
    const response = await fetch(
      `/metrics/error-rate?region=${region}&window=5m`
    );
    const data = await response.json();
    return data.rate;
  }

  private async getReplicationLag(region: string): Promise<number> {
    // Query replication status
    const response = await fetch(
      `/metrics/replication-lag?region=${region}`
    );
    const data = await response.json();
    return data.lagMs;
  }

  getHealthReport(): {
    global: 'healthy' | 'degraded' | 'unhealthy';
    regions: RegionHealth[];
  } {
    const regions = Array.from(this.health.values());

    const unhealthy = regions.filter((r) => r.status === 'unhealthy').length;
    const degraded = regions.filter((r) => r.status === 'degraded').length;

    let global: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
    if (degraded > 0) global = 'degraded';
    if (unhealthy > regions.length / 2) global = 'unhealthy';

    return { global, regions };
  }
}

Summary

Multi-region architecture decisions cascade through every layer:

AspectSingle RegionMulti-Region Active-PassiveMulti-Region Active-Active
LatencyHigh for distant usersMedium (regional reads)Low (local reads)
ConsistencyStrongStrong reads, eventual writesEventual (or complex)
ComplexityLowMediumHigh
CostLowMediumHigh
AvailabilitySingle point of failureFailover capableHighly available
Data sovereigntySimpleRegional storageComplex compliance

Key decisions:

  1. Edge vs. origin — What can run at the edge? Auth, personalization, caching.
  2. Read vs. write paths — Reads can be regional, writes often must be centralized.
  3. Consistency requirements — Not all data needs strong consistency.
  4. Conflict resolution — LWW is simple, CRDTs are correct, custom logic is necessary.
  5. Failure modes — What happens when regions partition?

The goal: minimize latency for reads (most common), maintain correctness for writes (most critical), and degrade gracefully when physics wins.

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.