Skip to content
All notes

Performance· 5 min read

The redirect hot path: caching lessons from building a URL shortener

Every millisecond of a redirect sits inside someone's navigation. Cache-aside with Redis, fire-and-forget analytics, and measuring what users actually feel.

A URL shortener is a deceptively good performance teacher because it has exactly one hot path, and that path sits inside the worst possible place: a human's navigation. When someone clicks a short link, your latency is added to a page load they're already waiting on. Nobody sees your UI. The redirect is the product.

Building EasyURL taught me more about caching discipline than platforms a hundred times its size, precisely because there was nowhere to hide.

Cache-aside, because misses must be survivable

The redirect lookup is a single key-to-URL mapping — the textbook case for Redis in front of the database. We used cache-aside rather than anything fancier: check Redis, miss, read the database, write Redis with a TTL, redirect. The reason isn't elegance, it's failure behavior. With cache-aside, Redis going down degrades you to database latency — slower, alive. Patterns where the cache is the only reader of truth turn a cache outage into a product outage.

Two details did the real work. Short links follow a power law — a tiny fraction of links take almost all the traffic — so even a modest cache holds nearly every hot key, and TTL expiry barely matters for hit rate. And negative caching mattered more than I expected: bots and typos hammer nonexistent slugs, and without caching the 'not found' answer, your misses go straight to the database in exactly the pattern an attacker would choose.

Analytics must never block the redirect

Every redirect logs a click — timestamp, referrer, rough geolocation. The naive version awaits the analytics write before redirecting, which means a slow analytics insert makes someone's page load slower. That's backwards: the user gets nothing from that write. Analytics became fire-and-forget — respond with the 301 immediately, record the click after the response is already gone.

Fire-and-forget forces an honest question: what happens when the write fails? For click analytics the answer is 'we lose one click', which is fine — and saying so out loud is the difference between a design decision and an accident. If the data were billing, the answer changes and so does the architecture. Deciding what you're allowed to lose is the actual engineering.

The hot path, shaped by priority
const cached = await redis.get(slug);
if (cached === NOT_FOUND) return res.status(404).render(notFound);
if (cached) {
  res.redirect(301, cached);       // user's wait ends here
  recordClick(slug, req).catch(log); // everything after is ours
  return;
}

Measure the click, not the server

My first benchmark was server processing time, and it said we were fast. It was also the wrong number. The user experiences DNS, TLS, the request, my lookup, and then the full load of the destination page. Measuring from the click taught me where the real budget went — and that a 301 with proper cache headers lets browsers and CDNs skip my server entirely on repeat clicks, which is the cheapest millisecond there is: the request that never arrives.

The habit that stuck: state the latency budget for the whole journey, then spend it where the user feels it. Optimizing the part you happen to control is comfortable. Optimizing the part the user experiences is the job.

What to take away

  • Choose cache patterns by their failure mode first — cache-aside degrades, cache-as-truth dies.
  • Cache negative results. Bots and typos will find your misses in exactly the worst pattern.
  • Nothing the user doesn't benefit from belongs before the response. Analytics is fire-and-forget, with data loss priced explicitly.
  • Measure latency from the user's click, not your server's clock.
  • The cheapest request is the one proper HTTP caching prevents from ever reaching you.