Design a URL Shortener
bit.ly at scale: turn long URLs into short codes, redirect in milliseconds from anywhere, count the clicks.
Difficulty: medium. Patterns: key-generation, caching, cdn, analytics. Reported at Amazon, Microsoft, Google, Adobe, Atlassian.
Study shows every answer; Practice hides them until you have produced your own.
Functional requirements
- Shorten a URL. Return a short code like sho.rt/x7Kq2. Optional custom alias and expiry.
- Redirect a short code to the original URL. HTTP 301 or 302 depending on whether clicks should be counted (302) or cached by browsers (301).
- Click analytics. Total clicks, clicks over time, referrer, country, device per link. Approximate counts are fine; the totals must not drift wildly.
- Link management. Owners can list, edit the destination, disable, or delete their links. Anonymous links are allowed with limits.
- Expiry. Links can expire at a time or after N clicks; expired codes return 410 Gone and are not reused for a long time.
- Abuse controls. Block known-malicious destinations, rate limit creation, and show an interstitial for suspicious links.
- Out of scope. QR code generation, branded domains per customer (mention how), and the full analytics dashboard UI.
Non-functional requirements
- Redirect latency (p99 < 50 ms globally). Redirects are served at the edge from cache. A redirect that goes to the origin database is the exception.
- Scale (100 M new links/month · 10 B redirects/month). Read to write ratio around 100:1. The write path is boring; the read path is a CDN problem.
- Availability (99.99 % for redirects). Links are embedded in emails, posters, and tweets for years. A redirect outage breaks the internet in small ways everywhere.
- Durability (no lost mappings). A short code that stops resolving is a broken link forever. Mappings are replicated across regions.
- Code length (7 characters). Base62, 62^7 ≈ 3.5 trillion codes. Short enough to type, large enough to never run out.
- Unpredictability. Sequential codes let anyone enumerate every link. Codes must not be guessable.
- Analytics freshness (minutes). Counts do not need to be real time; they must never block a redirect.
Back-of-envelope estimates
- Links created per second: ~40 avg · ~400 peak. 100 M/month ÷ 2.6 M s ≈ ~40/s. Peaks 10× during campaigns. A single database handles this trivially.
- Redirects per second: ~4 k avg · ~40 k peak. 10 B/month ÷ 2.6 M s ≈ ~3.9 k/s; viral spikes 10×+ on a single link. Served from CDN and cache; origin sees only misses.
- Total links after 5 years: ~6 B. 100 M/month × 60 months = 6 B mappings. 62^7 ≈ 3.5 T codes, so 7 characters uses under 0.2 % of the space and collisions are not a concern.
- Mapping storage: ~3 TB. Per link: code (7 B) + URL (~200 B avg, up to 2 KB) + owner, timestamps, flags (~50 B) + index overhead ≈ ~500 B. 6 B × 500 B = 3 TB. Fits a modest sharded store; a single node with SSDs could hold it, but not serve it globally.
- Cache hit rate needed: > 99 %. Redirect popularity is Zipfian: the top 1 % of links get ~90 % of clicks. A cache holding the hot ~50 M mappings (~25 GB) yields 99 %+ hits, so the origin sees ~40 misses/s average. The CDN edge in front of it catches most of that again.
- Click events per day: ~330 M. 10 B/month ÷ 30 = ~330 M events/day at ~200 B each (code, ts, referrer, UA hash, geo) ≈ 66 GB/day raw. Aggregate per link per hour and keep raw for 30 days.
- Key space check: 62^7 ≈ 3.5 T. 62 characters (a–z, A–Z, 0–9) to the power 7 = 3.52 × 10^12. At 100 M/month it would take 3 000 years to exhaust. Six characters (57 B) would last 47 years but is uncomfortably enumerable.
Components
- Browser / app: Follows short links (the read path) and, for owners, creates and manages links through the web app or API.
- CDN edge (edge cache + edge function): Terminates the redirect request close to the user. An edge function looks up the code in the edge KV cache and returns the redirect directly; on a miss it calls the redirect service. Also emits a click event to the analytics pipeline without waiting for it.
- Edge KV (replicated to PoPs): A globally replicated key-value store at the CDN (code → URL, flags, expiry). Populated on link creation and on misses; a few tens of GB covers the hot set everywhere.
- Redirect service: Origin for redirects: check the regional cache, then the mapping store; apply expiry, disabled, and safe-browsing flags; return 301/302/410. Stateless and replicated per region.
- Regional cache (Redis · code → mapping): Second cache tier per region, holding the hot tens of millions of mappings so the database only sees genuinely cold codes. Invalidated on edit and delete.
- Link API (create · manage): The write path: validate the URL, check the block list, allocate a code (or reserve the custom alias), write the mapping, warm caches, and return the short URL. Rate limited per user and IP.
- Key generation service (pre-generated ranges): Hands out unique, non-sequential 7-character codes without coordination on the hot path. Each API instance claims a range of counter values from the store and encodes them with a bijective scramble to base62.
- Mapping store (DynamoDB / Cassandra · key=code): code → { url, owner, created, expires, flags, click_limit }. Point lookups by code, so a key-value or wide-column store partitioned by code is ideal. Multi-region replication for durability and local reads. A secondary table indexes owner → codes for management pages.
- Range counter (small strongly consistent store): A single monotonic counter that hands out ranges of 10 000 ids atomically (ZooKeeper, a Postgres sequence, or DynamoDB atomic increment). Touched once per range, so its throughput is irrelevant.
- Click stream (Kafka): Click events from the edge and the redirect service, keyed by code. High volume, short retention. Feeds aggregation and raw archival.
- Analytics pipeline (stream aggregation): Aggregates clicks per code per hour by country, referrer, and device; maintains total click counts (also used to enforce click-limit expiry); writes raw events to cold storage.
- Analytics store (ClickHouse / warehouse): Hourly aggregates per code for dashboards, plus raw events for 30 days. Queried by owners, never by the redirect path.
- Safe-browsing checker (Google Safe Browsing · internal lists): Checked at creation and periodically re-checked for existing links. A flagged destination gets an interstitial warning or is disabled; the flag propagates through the caches.
User flows
- Follow a short link. The path that runs ten billion times a month. Almost all of it happens at the CDN edge; the origin only sees cold codes.
- Create a short link. Validate, allocate a code without a central hot spot, write once, warm the caches. Forty a second on average, which is easy; the design is about correctness and abuse.
- Edit, disable, or expire a link. Mappings are cached in several tiers, so a change is an invalidation problem. The design accepts a bounded propagation delay and makes expiry checkable everywhere.
- One link goes viral: 30 k clicks per second. A single hot key. The layered caches make it a non-event for the origin, and the analytics pipeline absorbs the burst through batching.
- Click analytics without touching the redirect path. Approximate, delayed, and completely decoupled. Every decision here protects redirect latency.
Deep dives
- Generating short codes. Hash the URL, random string, or counter? How do you get unique, short, unguessable codes with no hot spot?
- 301 vs 302 and the caching contract. Which redirect status do you return, and how does that interact with analytics and edits?
- Cache tiers and invalidation. Edge KV, regional Redis, and the database: why three tiers, and how do edits not serve stale destinations for hours?
- Choosing the mapping store. Six billion tiny rows, point lookups by code, global durability. SQL or key-value?
- Abuse: phishing, spam, and enumeration. Shorteners hide destinations, which is exactly what attackers want. How do you keep the service usable and not a phishing tool?