URL Shortener Design — Advanced Architecture and Performance Optimization
In this tutorial, you'll learn about URL Shortener Design. We cover key concepts, practical examples, and best practices.
Designing a URL shortener requires building a globally distributed system that handles millions of writes per day, billions of redirects, and provides sub-20 millisecond response times with strong consistency guarantees.
What You'll Learn
You'll master distributed ID generation with Snowflake, Bloom filter-based duplicate detection, hot-key caching strategies with consistent hashing, real-time analytics pipelines, and multi-region active-active deployment patterns.
Why It Matters
URL shorteners sit at the intersection of every link shared online. At TinyURL scale, 5 billion redirects per day means every millisecond of latency costs user engagement. At DodaTech, similar redirection patterns optimize deep linking in Doda Browser's privacy-focused link forwarding layer.
Real-World Use
Bitly processes 8 billion clicks monthly across 1.5 million customers. Each click generates analytics — referrer, device, location, time — feeding real-time dashboards. A single millisecond improvement in redirect latency saves 800,000 cumulative hours of user waiting per month.
System Architecture
flowchart TB
Client[User/Bot] -->|Short URL click| DNS[DNS Geo-Routing]
DNS --> LB[Global Load Balancer]
LB --> Regional[Regional Cluster]
subgraph Regional[Regional Deployment]
GW[API Gateway] --> Write[Write Service]
GW --> Redirect[Redirect Service]
GW --> Analytics[Analytics Service]
Write --> IDGen[Snowflake ID Gen]
Write --> Blocker[Bloom Filter]
Write --> DB[(CockroachDB Multi-Region)]
Redirect --> Cache[(Redis Cluster)]
Cache --> DB
Blocker --> Cache
Analytics --> Kafka[Kafka Stream]
Kafka --> StreamProc[Stream Processor]
StreamProc --> OLAP[(ClickHouse)]
end
Distributed ID Generation
Unlike the simple auto-increment approach, production URL shorteners use Snowflake-style IDs to avoid single-point-of-failure bottlenecks and enable multi-region writes.
import time
import threading
class SnowflakeGenerator:
def __init__(self, datacenter_id: int, worker_id: int):
self.datacenter_id = datacenter_id
self.worker_id = worker_id
self.sequence = 0
self.last_timestamp = -1
self.lock = threading.Lock()
self.epoch = 1700000000000
def next_id(self) -> int:
with self.lock:
timestamp = int(time.time() * 1000) - self.epoch
if timestamp < self.last_timestamp:
raise Exception("Clock moved backwards")
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & 4095
if self.sequence == 0:
timestamp = self._wait_next_ms()
else:
self.sequence = 0
self.last_timestamp = timestamp
return (timestamp << 22) | (self.datacenter_id << 17) | (self.worker_id << 12) | self.sequence
def _wait_next_ms(self):
ts = int(time.time() * 1000) - self.epoch
while ts <= self.last_timestamp:
ts = int(time.time() * 1000) - self.epoch
return ts
Expected behavior: Each generator produces 4 million unique IDs per second per worker without coordination. IDs are globally unique, time-sortable, and 64-bit integers that encode directly to 7-character Base62 strings.
Bloom Filter for Duplicate Detection
Checking for duplicate URLs with a database query every time is expensive at 10,000 writes per second. A Bloom filter provides probabilistic duplicate detection with constant memory.
import mmh3
import math
class BloomFilter:
def __init__(self, capacity: int = 100_000_000, error_rate: float = 0.001):
self.size = int(-capacity * math.log(error_rate) / (math.log(2) ** 2))
self.hash_count = int(self.size / capacity * math.log(2))
self.bit_array = bytearray(math.ceil(self.size / 8))
def _hashes(self, item: str):
h1 = mmh3.hash128(item, 0)
h2 = mmh3.hash128(item, 1)
for i in range(self.hash_count):
yield (h1 + i * h2) % self.size
def add(self, item: str):
for h in self._hashes(item):
self.bit_array[h // 8] |= 1 << (h % 8)
def might_contain(self, item: str) -> bool:
for h in self._hashes(item):
if not (self.bit_array[h // 8] & (1 << (h % 8))):
return False
return True
bloom = BloomFilter()
shortened_urls = {}
def shorten_url(long_url: str) -> str:
if bloom.might_contain(long_url):
existing = shortened_urls.get(long_url)
if existing:
return existing
url_id = snowflake.next_id()
short_code = base62_encode(url_id)
shortened_urls[long_url] = short_code
bloom.add(long_url)
db.execute("INSERT INTO urls (id, short_code, long_url) VALUES (%s, %s, %s)", url_id, short_code, long_url)
return short_code
Expected behavior: The Bloom filter uses approximately 170 MB for 100 million URLs with 0.1 percent false positive rate. Every URL is checked in microseconds without touching the database.
Hot-Key Caching with Consistent Hashing
Some short URLs receive millions of clicks (celebrity links, Super Bowl ads). These hot keys need dedicated cache slots to avoid cache stampedes.
import hashlib
class ConsistentHashRing:
def __init__(self, nodes: list[str], replicas: int = 150):
self.replicas = replicas
self.ring = {}
self.sorted_keys = []
for node in nodes:
for i in range(replicas):
key = self._hash(f"{node}:{i}")
self.ring[key] = node
self.sorted_keys.append(key)
self.sorted_keys.sort()
def _hash(self, key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def get_node(self, key: str) -> str:
if not self.sorted_keys:
return None
h = self._hash(key)
for ring_key in self.sorted_keys:
if ring_key >= h:
return self.ring[ring_key]
return self.ring[self.sorted_keys[0]]
def get_hot_key_nodes(self, hot_keys: list[str]) -> dict:
return {k: self.get_node(f"hot:{k}") for k in hot_keys}
ring = ConsistentHashRing(["cache-1:6379", "cache-2:6379", "cache-3:6379"])
hot_keys = ["superbowl-ad", "election-results", "product-launch"]
print(ring.get_hot_key_nodes(hot_keys))
Expected behavior: Hot keys are isolated to dedicated cache nodes to prevent popular URLs from evicting less popular ones. Consistent hashing minimizes redistribution when nodes are added or removed.
Redirect Response Strategy
| Status Code | Browser Cache | Analytics | Use Case |
|---|---|---|---|
| 301 Moved Permanently | Cached indefinitely | Lost after first hit | Permanent links, no analytics needed |
| 302 Found | Not cached | Every click recorded | Analytics tracking, A/B testing |
| 307 Temporary Redirect | Not cached, preserves method | Every click recorded | POST-based APIs |
Production URL shorteners default to 302 for analytics and switch to 301 after 30 days for active links that have stable traffic patterns sorted.
Analytics Pipeline
-- Click events streamed via Kafka
CREATE TABLE click_events (
short_code VARCHAR(10),
timestamp DateTime,
referrer String,
user_agent String,
ip_address String,
country String,
device_type String,
browser String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (short_code, timestamp);
-- Materialized view for per-link daily stats
CREATE MATERIALIZED VIEW link_daily_stats
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (short_code, day)
AS SELECT
short_code,
toDate(timestamp) AS day,
count() AS clicks,
uniq(ip_address) AS unique_visitors,
uniq(referrer) AS unique_referrers
FROM click_events
GROUP BY short_code, day;
Common Errors
1. Synchronous ID Generation
Auto-increment databases become write bottlenecks. Distributed Snowflake generators run on every write node without coordination.
2. Missing Bloom Filter Fallback
Bloom filters have false positives. Always verify with a database lookup when the filter reports a potential duplicate.
3. Single-Region Deployment
DNS-level redirects from Australia to US-East add 150ms latency. Deploy active-active in at least three regions.
4. Cache-Aside Without Hot Key Detection
Without DCP (distributed cache population) patterns for hot keys, every celebrity link causes a cache stampede that overloads the database.
5. Analytics in the Critical Path
Writing click data synchronously during redirects doubles latency. Use async Kafka producers with local buffering.
6. No Rate Limiting
Without rate limiting, malicious users can exhaust the ID space or overload the system. Limit to 10 shorten requests per IP per second using a sliding window counter.
7. Ignoring Custom Alias Validation
Custom aliases like "admin", "api", or "login" must be reserved. Validate against a reserved-word set during alias creation.
Practice Questions
1. Why use Snowflake IDs over auto-increment for URL shorteners?
Auto-increment requires a single database master, creating a single point of failure and limiting write throughput. Snowflake generates unique IDs across thousands of nodes without coordination, supporting multi-region active-active writes.
2. How does a Bloom filter help with duplicate URL detection?
A Bloom filter provides a memory-efficient, probabilistic membership test. It eliminates 99.9 percent of database lookups for URLs that have never been shortened, reducing write path latency from 10ms to microseconds.
3. What is the difference between 301 and 302 redirects in this context?
301 responses are cached by browsers forever after the first hit, reducing server load but losing analytics. 302 responses always hit the server, enabling click tracking but increasing redirect latency by approximately 5-10ms.
4. How do you handle hot-key cache stampedes?
Detect hot keys by tracking access frequency, promote them to dedicated cache nodes, pre-warm on predicted traffic spikes, and implement a circuit breaker that falls back to database reads if the cache tier is overloaded.
5. Challenge: Implement a distributed rate limiter for the shorten endpoint that uses a sliding window counter stored in Redis sorted sets. Each IP address gets 10 requests per 60-second window. Return HTTP 429 when exceeded.
Mini Project: URL Shortener Analytics Dashboard
Build a complete URL shortener with:
- A Snowflake ID generator service exposing a gRPC
GetIdmethod - A write service with Bloom filter duplicate detection, rate limiting, and async analytics logging
- A redirect service with consistent hashing-based hot key caching
- A ClickHouse-backed analytics service with daily materialized views
- A dashboard showing per-link click counts, unique visitors, referrer breakdown, and geographic distribution
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro