· SWE Editorial · System Design  · 7 min read

Design a URL Shortener: Scaling Bottlenecks

Everyone can design a URL shortener that works for 10 requests a second. Here's what actually breaks when you hit 10,000 requests a second: database hotspots, cache stampedes, hash collisions, and the geographic latency problem nobody mentions in the whiteboard round.

Everyone can design a URL shortener that works for 10 requests a second. Here's what actually breaks when you hit 10,000 requests a second: database hotspots, cache stampedes, hash collisions, and the geographic latency problem nobody mentions in the whiteboard round.

Why “just use a hash function” isn’t a real answer

Ask a candidate to design a URL shortener and within two minutes you’ll hear: “we take the long URL, run it through MD5, base62-encode the first 7 characters, done.” That answer gets you through the first five minutes of the interview and then the interviewer says the four words that separate a pass from a strong hire: “now scale it to a billion links.”

That’s the actual interview. The encoding scheme is a formality. The scaling story is the signal. This article walks through the bottlenecks that show up once a URL shortener leaves the whiteboard: database hotspots, cache stampedes, hash collisions at scale, geographic distribution, and CDN strategy — the five places where naive designs fall over in production.

The baseline design, quickly

Before the bottlenecks, the shape of the system:

Client -> Load Balancer -> App Servers -> Cache (Redis) -> DB (sharded key-value or SQL)
                                              |
                                        Counter/ID Service
  • Write path: client submits a long URL, service generates a short code, stores the mapping, returns the short URL.
  • Read path: client hits short.ly/abc123, service looks up the long URL, issues a 301/302 redirect.

Reads outnumber writes by roughly 100:1 in most link-shortening products — this ratio is the reason the caching story matters more than the write-path story.

Bottleneck 1: database hotspots

The naive approach auto-increments an ID and base62-encodes it. Sequential IDs sound clean, but they create a nasty side effect: if you shard by ID range, all new writes land on the same shard — the one holding the current high-water mark. Every write, every cache-miss read for a recently created link, and every “trending link” burst all pile onto one node while the other nine shards sit idle.

Fixes that come up in strong interviews:

  • Range-based ID allocation with pre-fetched blocks. Each app server reserves a block of 1,000 IDs from a central counter service (backed by Zookeeper, etcd, or a dedicated DB row with an atomic increment) and hands them out locally. This turns one contended global counter into occasional low-frequency contention.
  • Shard by hash of the short code, not by ID range. This spreads both the write load and the natural “hot link” read load across shards, at the cost of losing sortable IDs (which you rarely need anyway).
  • Separate hot and cold storage. Recently created and frequently accessed links live in a smaller, cache-friendly tier; long-tail links fall through to cheaper, colder storage.

The interview signal here is recognizing that “auto-increment ID” is a modeling choice with a distributed-systems consequence, not just a database detail.

Bottleneck 2: cache stampede

A link goes viral — a celebrity tweets a shortened URL, and it gets 50,000 requests per second. The cache entry for that key expires (TTL eviction) at the exact moment traffic is peaking. Every one of those 50,000 requests misses the cache simultaneously, and all of them hit the database at once, looking for the same row. The database, which was fine handling steady-state traffic, falls over from a thundering herd for a single key.

This is the cache stampede problem, and it’s one of the highest-value things to bring up unprompted in a system design interview because most candidates never mention it.

Mitigations, roughly in order of how often they come up:

  1. Request coalescing / single-flight. The first request that misses the cache acquires a short-lived lock (or uses a library like Go’s singleflight) and fetches from the DB; concurrent requests for the same key wait on that result instead of independently querying the DB.
  2. Probabilistic early expiration. Instead of a hard TTL, recompute the value slightly before expiry with a probability that increases as the TTL approaches — spreads refreshes out instead of bunching them at a fixed instant.
  3. Never-expire hot keys with active invalidation. For known-hot links, skip TTL-based eviction entirely and invalidate the cache entry explicitly when the underlying data changes (rare for redirect mappings, which are near-immutable).
  4. Stale-while-revalidate. Serve the slightly-stale cached value immediately while a background job refreshes it — appropriate here because a redirect target being one second stale is imperceptible to users.

Bottleneck 3: hash collision at scale

If you generate short codes by hashing the long URL (MD5, SHA-256, then truncating to 6-7 base62 characters), collisions are inevitable at scale — birthday-paradox math means with a 7-character base62 space (~3.5 trillion combinations) you’d expect meaningful collision rates well before you fill the space, especially if multiple users shorten the same URL and you want each to get a distinct code.

What interviewers want to hear:

  • Detect and retry. On collision, append a counter or re-salt the hash and rehash. Simple, but adds latency on the (rare) collision path — acceptable since collisions should be uncommon if the code length is chosen correctly.
  • Counter-based generation avoids collision entirely. If you go back to bottleneck #1’s block-allocation scheme, collisions become structurally impossible — every ID is unique by construction, and you base62-encode that instead of hashing the URL. This is why most production systems (Bitly-style) use counters, not hashes, for the code itself.
  • Custom alias handling is separate. User-chosen vanity codes (“mybrand/promo”) need a uniqueness check against the same keyspace, typically via a unique index and an insert-or-conflict pattern.

A strong candidate explicitly compares hash-based vs. counter-based generation and picks counter-based specifically because it sidesteps the collision problem, rather than treating collision handling as an afterthought bolted onto a hash scheme.

Bottleneck 4: geographic distribution

A single-region deployment means every redirect for a European user round-trips to a US data center. For a product whose entire value proposition is a fast redirect, 150-200ms of pure network latency before the 301 even fires is a real product defect, not a nitpick.

ApproachLatency for distant usersConsistency complexityCost
Single regionHigh (150-250ms RTT)NoneLow
Multi-region read replicasLow for readsRead replicas lag; writes still centralizedMedium
Multi-region active-activeLow for reads and writesConflict resolution needed (rare here since codes are unique)High
Edge cache only (origin stays single-region)Low for cached links; high for cache missesLow — cache is disposableLow-Medium

For a URL shortener specifically, full active-active writes are usually overkill — write volume is low and writes aren’t latency-sensitive (nobody notices if link creation takes 200ms). The pragmatic answer: keep the write path centralized or lightly multi-region, and push almost all the latency-sensitive work — the redirect read — to the edge.

Bottleneck 5: CDN strategy

This is the answer that separates senior candidates. A URL redirect is just an HTTP 301/302 response with a Location header — there’s no reason it needs to touch an origin server at all for a cached link. Redirects can be served directly from CDN edge nodes (Cloudflare Workers, CloudFront Functions, Fastly Compute) with the mapping cached at the edge.

User (Tokyo) -> Nearest CDN Edge Node
                   |
             cache hit? -> serve 301 directly from edge (no origin round trip)
                   |
             cache miss -> fetch from origin, cache at edge, serve 301

This collapses the “geographic distribution” problem and the “cache stampede” problem into one clean answer: edge caching with short-lived stampede protection at each edge node (since each edge PoP is its own cache with its own miss storm potential). The tradeoff to name explicitly: analytics and click tracking, which used to happen naturally at the origin, now need to be captured at the edge (via edge function logging or async beacon) or accepted as slightly lossy.

Putting it together

The system that survives the “now scale it” follow-up looks like this: counter-based ID generation with pre-allocated blocks (no hotspot, no collisions), hash-based sharding for reads once IDs exist, request coalescing plus probabilistic expiration to kill stampedes, and CDN-edge redirect serving so geography stops being a latency problem at all. None of this is exotic — it’s five well-known techniques applied to the right layer, and naming all five unprompted is what makes an interview memorable.

Further reading

For a structured walkthrough of this exact question plus 30+ others asked at FAANG and top-tier startups, with the follow-up questions interviewers actually ask after the whiteboard answer, see The 0-to-1 SWE Interview Playbook (Amazon: https://www.amazon.com/dp/B0H256Z1MF?tag=sirjohnnymai-20). It covers the scaling-bottleneck pattern used here — hotspots, stampedes, collisions, geography, edge — as a repeatable framework you can apply to rate limiters, chat systems, and news feeds, not just URL shorteners.

Back to Blog

Related Posts

View All Posts »

Design a Rate Limiter: Architecture and Algorithms

Token bucket vs. leaky bucket vs. sliding window log — the algorithm choice is only half the design. This piece is about where the limiter physically sits in your request path, and why that placement decision matters as much as the algorithm itself.