URL Shortener: The Read Path
Design a URL shortener read path with cache-hit capacity math, miss coalescing, redirect contracts, bounded failures, and asynchronous analytics.
What is a URL-shortener read path?
A URL-shortener read path is the sequence that turns a short-code request such as GET /r7K2 into an HTTP redirect to the stored destination. It begins when a client reaches the service and ends when the client receives a redirect response. The common path should be short, read-heavy, and safe when caches or dependencies fail.
This path matters because one link can be created once and opened millions of times. If redirects outnumber creations by about 100 to 1, read latency, cache behavior, and failure isolation dominate the user experience and infrastructure cost.
The core invariant is simple: every valid short code resolves to the intended destination without making analytics, cache warm-up, or a retry storm part of the user's critical path.
This lesson builds on Scalability Basics. Review it first if horizontal scaling, latency percentiles, or caching are unfamiliar.
Trace one redirect before adding scale
Start with the request that must succeed. A client sends a short code, the service validates it, a cache or durable store supplies the destination, and the service returns an HTTP redirect. Click analytics can be emitted after the response decision.
The redirect critical path
The cache-hit route is the common path. A cache miss may consult durable storage, while analytics leaves the user-facing latency budget.
GET /r7K2
Client
Send the short code with a finite deadline. Follow a redirect only after validating the response and destination policy.
Validate and route
Edge and redirect service
Normalize the code, reject malformed input, apply abuse controls, and look up the mapping without keeping request-local state.
Common path
Mapping cache
Return hot mappings in a few milliseconds. Cache misses and expired entries continue to the source of truth.
Miss path
Durable mapping store
Return the current mapping, expiration, and state. The service populates the cache before returning the redirect.
Off path
Analytics stream
Receive a click event asynchronously. Counting can lag briefly without delaying or blocking the redirect.
The handler makes four bounded decisions:
- Validate the code and reject missing, expired, disabled, or abusive requests.
- Read the mapping cache and distinguish a hit from a miss.
- On a miss, read durable storage once, populate the cache, and return the same redirect contract.
- Publish analytics asynchronously with a request identifier so consumers can deduplicate retries.
Turn the read ratio into a capacity model
A read-heavy ratio does not automatically guarantee a healthy design. The origin load depends on the cache miss ratio:
origin reads per second = redirect requests per second x (1 - cache hit rate)
At 100,000 redirects per second, a 99% hit rate sends about 1,000 reads per second to storage. A 90% hit rate sends 10,000. The percentage changed by only nine points, but origin demand grew tenfold.
100:1
Read-to-write ratio
Redirects dominate link creation
99%
Target hit rate
One miss per 100 redirects
1,000/s
Origin reads
At 100,000 redirects/s
<100 ms
Redirect p99
Measured at the service boundary
Use the lab to change traffic, cache hit rate, and miss protection. Watch the storage demand and latency budget move together.
See when a small miss ratio becomes a large origin load
Change traffic, hit rate, and hot-key protection. The model keeps the redirect total fixed while exposing storage demand and burst risk.
Loading the read-path model...
The model exposes three operational lessons:
- Percentages hide absolute load. A 95% hit rate still creates 15,000 origin reads per second at 300,000 redirects per second.
- Bursts matter. Popular links can expire together and turn one hot key into many concurrent origin reads.
- The miss path is production traffic. It needs bounded concurrency, request coalescing, timeouts, and tested capacity.
Protect the cache without confusing it for storage
The cache accelerates reads; it does not own the mapping. Durable storage remains the authority for destination, expiration, and disabled state.
Lookup
Read through
Read the cache first. On a miss, load one authoritative record, populate the cache with a bounded TTL, and answer the request.
Hot key
Coalesce misses
Let one request refill an expired key while matching requests await that result. This limits duplicate origin work during bursts.
Distribution
Jitter expiry
Vary TTLs within a safe range so many popular mappings do not expire in the same instant and create synchronized load.
Negative result
Cache absence
Briefly cache a verified not-found or expired state. Use a short TTL so later creation or reactivation is not hidden for long.
Cache policy must preserve correctness:
- A disabled or expired link must stop redirecting within the product's stated propagation window.
- A destination edit needs explicit invalidation or a versioned key; waiting for an arbitrary long TTL is not a release plan.
- A cache outage must not unlock unbounded direct reads to storage.
- A stale response, if allowed during an incident, must be bounded by age and link state.
- Cache keys must include tenant or namespace boundaries when codes are not globally unique.
Make the HTTP redirect an explicit contract
The response code is not a cosmetic choice. Browsers, crawlers, and intermediary caches can remember permanent redirects, and some status codes allow an HTTP method to change when followed.
301 Moved Permanently: suitable only when the destination is stable enough for clients and caches to remember it beyond your immediate control.302 Found: a conservative default for changeable links reached withGET; clients treat the destination as temporary.307 Temporary Redirect: temporary like302, but explicitly preserves the original HTTP method and body.404 Not Foundor410 Gone: use a terminal response for missing or expired mappings instead of redirecting to an invented destination.
Choose a link lifecycle, redirect status, and analytics placement in the contract lab. The result explains what the client can cache and what work remains on the critical path.
Choose what the client may remember and what must stay off path
Match the link lifecycle to an HTTP status, then place analytics. The visible result combines mutability, method behavior, latency, and failure coupling.
Loading redirect semantics...
The safest default for an editable marketing or user-created link is usually a temporary redirect with analytics emitted asynchronously. A permanent redirect can be more cacheable, but that optimization transfers control to clients that may not promptly observe later destination changes.
Design the failures before they happen
A healthy cache hit is only one state. Review the user-visible result and the protection boundary for each failure.
1 Recover
Cache miss
Load the authoritative mapping once, populate the cache with bounded TTL and jitter, then return the redirect.
2 Contain
Cache unavailable
Allow only a bounded number of requests to reach storage. Shed excess work or use policy-approved stale data instead of collapsing the source of truth.
3 Bound
Store slow
Use a deadline shorter than the client budget, avoid unsafe retries, and return a controlled error when no trustworthy mapping is available.
4 Decouple
Analytics delayed
Keep redirecting, buffer or retry events asynchronously, and monitor event age. Analytics loss must not become redirect loss.
Watch these failure signals together:
- redirect p50, p95, and p99 latency by cache result;
- cache hit rate, eviction rate, and refill duration;
- origin reads, throttled reads, queue age, and timeout ratio;
- missing, expired, disabled, and stale-link responses;
- click-event publish failures, backlog age, and consumer deduplication;
- hot-key concentration and coalesced waiter count.
Do not retry the complete redirect path without a deadline. Retries can multiply cache misses, origin reads, and analytics events while the user still receives no redirect.
Implement a bounded read-through handler
The handler example keeps the authoritative lookup bounded, coalesces concurrent misses for one code, distinguishes negative results, and emits analytics after the response decision.
Cache entries need a policy as well as a value. The second example applies TTL jitter and keeps negative entries short-lived so absent-code traffic cannot overload storage without hiding a later valid mapping for too long.
Before release, prove all of the following:
- cache hits and misses return the same destination and redirect status;
- concurrent misses for one hot code produce one authoritative read per service instance;
- expired and disabled mappings never become successful redirects;
- destination edits invalidate or version the cached mapping within the promised window;
- analytics retries do not duplicate business counts;
- a cache outage cannot exceed the storage concurrency budget;
- timeout and shed responses are observable and do not trigger an unbounded retry loop.