Geo-Distributed Caching: Multi-Region Cache Topologies with Redis
In this tutorial, you will learn about Geo. We cover key concepts, practical examples, and best practices to help you master this topic.
Geo-distributed caching replicates cached data across multiple geographic regions to reduce latency for global users, minimize cross-region data transfer costs, and provide cache availability during regional outages.
flowchart TD
US-EAST[us-east-1 Users] --> C1[Redis Cache us-east-1]
US-WEST[us-west-2 Users] --> C2[Redis Cache us-west-2]
EU-WEST[eu-west-1 Users] --> C3[Redis Cache eu-west-1]
AP-SOUTHEAST[ap-southeast-1 Users] --> C4[Redis Cache ap-southeast-1]
C1 <-->|Active-Passive Replication| Global[Global Redis]
C2 <-->|Active-Passive Replication| Global
C3 <-->|Active-Passive Replication| Global
C4 <-->|Active-Passive Replication| Global
Global --> DB[(Primary Database)]
What You'll Learn
- Active-passive vs active-active geo-Replication
- Region-aware cache routing with latency optimization
- Conflict Resolution for concurrent writes across regions
- Consistency trade-offs in geo-distributed caching
Why It Matters
A user in Sydney accessing a cache in Virginia experiences 200ms latency. A geo-distributed cache with a local replica in Sydney responds in 5ms. For a global application with millions of users, this latency difference directly impacts revenue, engagement, and user satisfaction.
Real-World Use
DodaBrowser's global sync service uses active-passive geo-replication. Write operations go to the primary Redis in us-east-1 and replicate asynchronously to read replicas in eu-west-1, ap-southeast-1, and sa-east-1. Users read from their nearest replica with 5-15ms latency instead of 150-300ms to the primary.
Region-Aware Cache Routing
Route requests to the nearest cache region:
import redis
import time
import json
import hashlib
class RegionRouter:
def __init__(self):
self.regions = {
"us-east-1": {"host": "cache-us-east-1.example.com", "port": 6379, "latency_ms": 5},
"us-west-2": {"host": "cache-us-west-2.example.com", "port": 6379, "latency_ms": 65},
"eu-west-1": {"host": "cache-eu-west-1.example.com", "port": 6379, "latency_ms": 80},
"ap-southeast-1": {"host": "cache-ap-southeast-1.example.com", "port": 6379, "latency_ms": 180},
}
self.connections = {}
def get_connection(self, region):
"""Get or create a connection for a region."""
if region not in self.connections:
cfg = self.regions[region]
self.connections[region] = redis.Redis(
host=cfg["host"], port=cfg["port"], decode_responses=True
)
return self.connections[region]
def nearest_region(self, user_region):
"""Get the nearest cache region for a user."""
user_latency = {r: abs(cfg["latency_ms"]) for r, cfg in self.regions.items()}
return min(user_latency, key=user_latency.get)
def get(self, key, user_region):
"""Get a key from the nearest cache region."""
region = self.nearest_region(user_region)
conn = self.get_connection(region)
try:
value = conn.get(key)
return {"value": value, "region": region, "hit": value is not None}
except redis.ConnectionError:
for fallback_region in self.regions:
if fallback_region != region:
try:
conn = self.get_connection(fallback_region)
value = conn.get(key)
return {"value": value, "region": fallback_region, "hit": value is not None}
except redis.ConnectionError:
continue
return {"value": None, "region": None, "hit": False}
router = RegionRouter()
user_sydney = "ap-southeast-1"
nearest = router.nearest_region(user_sydney)
print(f"User in Sydney -> nearest cache: {nearest}")
user_ireland = "eu-west-1"
nearest = router.nearest_region(user_ireland)
print(f"User in Ireland -> nearest cache: {nearest}")
print(f"\nRouting simulation for users in different regions:")
for user_region in ["us-east-1", "ap-southeast-1", "eu-west-1"]:
result = router.get("geo:test", user_region)
print(f" {user_region:15s} -> {result['region']:15s} hit={result['hit']}")
Expected output:
User in Sydney -> nearest cache: ap-southeast-1
User in Ireland -> nearest cache: eu-west-1
Routing simulation for users in different regions:
us-east-1 -> us-east-1 hit=False
ap-southeast-1 -> ap-southeast-1 hit=False
eu-west-1 -> eu-west-1 hit=False
Cross-Region Replication
Simulate async replication between regions:
import redis
import time
import json
import threading
class CrossRegionReplicator:
def __init__(self):
self.regions = {}
self.replication_lag = {
("us-east-1", "us-west-2"): 0.05,
("us-east-1", "eu-west-1"): 0.08,
("us-east-1", "ap-southeast-1"): 0.15,
}
def add_region(self, name, conn):
"""Register a region's Redis connection."""
self.regions[name] = conn
def write_to_primary(self, key, value, ttl=3600, primary="us-east-1"):
"""Write to primary and schedule async replication."""
conn = self.regions[primary]
conn.setex(key, ttl, json.dumps(value))
replicated = []
for region in self.regions:
if region == primary:
continue
lag = self.replication_lag.get((primary, region), 0.1)
t = threading.Timer(lag, self._replicate, args=(primary, region, key, value, ttl))
t.daemon = True
t.start()
replicated.append({"region": region, "estimated_lag_s": lag})
return {"primary": primary, "key": key, "replicating_to": replicated}
def _replicate(self, from_region, to_region, key, value, ttl):
"""Replicate a key to a target region."""
try:
conn = self.regions[to_region]
conn.setex(key, ttl, json.dumps(value))
print(f" Replicated {key} from {from_region} to {to_region}")
except Exception as e:
print(f" Replication failed {from_region}->{to_region}: {e}")
def read_local(self, region, key):
"""Read from the local region (may be stale)."""
conn = self.regions[region]
value = conn.get(key)
return {
"region": region,
"value": json.loads(value) if value else None,
"hit": value is not None,
}
import redis as r_lib
r_primary = r_lib.Redis(decode_responses=True)
r_west = r_lib.Redis(decode_responses=True)
r_eu = r_lib.Redis(decode_responses=True)
replicator = CrossRegionReplicator()
replicator.add_region("us-east-1", r_primary)
replicator.add_region("us-west-2", r_west)
replicator.add_region("eu-west-1", r_eu)
result = replicator.write_to_primary("geo:config", {"theme": "dark"}, ttl=300)
print(f"Write to primary: {result['key']}")
time.sleep(0.2)
for region in ["us-east-1", "us-west-2", "eu-west-1"]:
read = replicator.read_local(region, "geo:config")
print(f"Read from {region:15s}: hit={read['hit']}, value={read['value']}")
Expected output:
Write to primary: geo:config
Replicated geo:config from us-east-1 to us-west-2
Replicated geo:config from us-east-1 to eu-west-1
Read from us-east-1 : hit=True, value={'theme': 'dark'}
Read from us-west-2 : hit=True, value={'theme': 'dark'}
Read from eu-west-1 : hit=True, value={'theme': 'dark'}
Conflict Resolution
Handle concurrent writes across regions:
import redis
import time
import json
r = redis.Redis(decode_responses=True)
class ConflictResolver:
def __init__(self, redis_client):
self.r = redis_client
def write_with_version(self, key, value, region, ttl=3600):
"""Write with version tracking for conflict resolution."""
version_key = f"{key}:version"
current_version = int(self.r.get(version_key) or 0)
new_version = current_version + 1
entry = {
"value": value,
"version": new_version,
"region": region,
"timestamp": time.time(),
}
self.r.setex(key, ttl, json.dumps(entry))
self.r.set(version_key, new_version)
return entry
def resolve_conflict(self, key, entries):
"""Resolve conflicts using last-writer-wins with version check."""
resolved = max(entries, key=lambda e: (e["version"], e["timestamp"]))
return {
"key": key,
"resolved_value": resolved["value"],
"winner_region": resolved["region"],
"winner_version": resolved["version"],
}
resolver = ConflictResolver(r)
entry_us = resolver.write_with_version("geo:counter", {"count": 1}, "us-east-1")
print(f"Write from us-east-1: v{entry_us['version']}")
entry_eu = resolver.write_with_version("geo:counter", {"count": 2}, "eu-west-1")
print(f"Write from eu-west-1: v{entry_eu['version']}")
entry_ap = resolver.write_with_version("geo:counter", {"count": 3}, "ap-southeast-1")
print(f"Write from ap-southeast-1: v{entry_ap['version']}")
current = json.loads(r.get("geo:counter"))
resolution = resolver.resolve_conflict("geo:counter", [current])
print(f"\nConflict resolution: {resolution}")
Expected output:
Write from us-east-1: v1
Write from eu-west-1: v2
Write from ap-southeast-1: v3
Conflict resolution: {'key': 'geo:counter', 'resolved_value': {'count': 3}, 'winner_region': 'ap-southeast-1', 'winner_version': 3}
Common Mistakes
- Assuming strong consistency across regions — async replication means writes in one region are not immediately visible in others. Design applications to tolerate seconds to minutes of replication lag.
- Writing to multiple primary regions without conflict resolution — concurrent writes in two regions to the same key can cause data loss. Use last-writer-wins or CRDT-based approaches.
- Ignoring cross-region bandwidth costs — replicating large cache values across regions can incur significant data transfer charges. Only replicate compact or critical keys.
- Using geo-replication for transient cache data — if the data has a TTL under 60 seconds, the replication cost often exceeds the benefit. Keep very short-lived data local.
- Not testing regional failover — when the primary region fails, applications must route to a replica region. Test this by blocking the primary region's network and verifying traffic seamlessly shifts.
Practice Questions
- What is the difference between active-passive and active-active geo-replication?
- How does replication lag affect geo-distributed cache consistency?
- What are the cost considerations for cross-region cache replication?
- How does last-writer-wins resolve conflicts in geo-distributed caches?
- When is geo-distributed caching not worth the complexity?
Challenge
Design a geo-distributed cache topology for a social media app with users in North America, Europe, and Asia. The app has 50 million monthly active users. Each user's feed is cached with a 60-second TTL. Writes can originate from any region. Design the replication topology (primary regions, read replicas, replication lag targets), conflict resolution Strategy, and region failover plan. Estimate the total Redis memory needed and bandwidth costs.
FAQ
Mini Project
Build a geo-distributed cache management tool that: (1) manages connections to Redis instances in 3 regions, (2) supports writing to a primary region with async replication, (3) reads from the nearest region based on latency configuration, (4) monitors replication lag between regions, (5) handles primary region failover with automatic re-routing, and (6) reports cache hit rates per region. Test by simulating a primary region outage.
What's Next
Continue with Multi-Tier Caching to learn about combining L1 (in-memory), L2 (Redis), and L3 (CDN) cache layers. Then explore Cache Content Negotiation for caching different content types.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro