Why a redirect belongs on the CDN, not your app server
A short link is a cache entry with a database behind it. Once we treated it that way, the interesting work stopped being the lookup and started being invalidation.
The old path had four hops too many
Our first version resolved every click in the application: TLS terminated at the load balancer, the request reached a Go service, the service asked Postgres for the slug, and the answer came back as a 302. Correct, and slower than it needed to be. A click from Frankfurt to a database in one region pays for the distance twice, and the answer is the same for every visitor who follows that link.
The redirect is the most read-heavy, least personalised thing we serve. It has no session, no body and no reason to be computed more than once. That is the definition of a cacheable response, so we moved it to the edge.
Two cache windows, not one
Hits and misses want different lifetimes. A resolved slug is stable, so it caches for a long time. A slug that does not exist yet is a different story — someone may be about to create it — so a miss caches briefly, long enough to absorb a scan, short enough that a new link works almost immediately.
HIT Cache-Control: public, max-age=2592000 # 30 days
MISS Cache-Control: public, max-age=60 # 1 minute
Everything the edge needs is in the URL, so a hit never touches origin. Origin requests are authenticated with a shared key that supports a previous value, which is what makes rotating it a non-event rather than an outage.
Then invalidation becomes the product
A thirty-day cache is only acceptable if editing a link takes effect now. When a destination changes, the write enqueues a purge in the same transaction as the update, and a worker drains that queue with retries and a stale-job warning. If the CDN is briefly unavailable, the job waits; it does not vanish.
The rule we settled on: a cache entry may be stale for as long as you like, provided nobody can change it without the purge being written down first.
What it bought
Redirect latency now sits in single-digit milliseconds at the median, and it barely moves with traffic, because the database is no longer in the request path. Analytics stayed behind: counting a click is a write, and writes do not belong at the edge in the same way reads do. Ingesting counts from edge logs is the next piece of work, and until it lands, the honest description is that redirects are fast and counting is eventually consistent.
The general lesson is dull and reliable: find the response that is identical for everyone, move it as close to the visitor as you can, and spend the saved effort on making invalidation boring.