URL Shortener — Failure & Scale Scenarios
Failure scenarios expose the real resilience of a URL shortener architecture. Each scenario below consistently covers trigger, immediate effect, user-visible impact, detection signals, graceful degradation, recovery, and prevention — covering dependency failures, races, stale state, abuse, and overload, not only outright outages.
Redis Cache Failure
The distributed cache becomes completely unavailable. Every request that would have hit it now falls through to the database — do not assume the database can simply absorb that shift.
Trigger
Redis node/cluster crash, network partition between app servers and cache, or an operator-initiated failover with a gap in coverage.
Immediate system effect
All reads that would have been cache hits now query the database instead. Depending on the prior cache hit ratio, this can be a large multiplicative increase in database read load — if the cache was absorbing the large majority of traffic, the database could receive many times its steady-state read volume almost instantly. Do not assume this increase is automatically survivable; it depends on how much headroom the database and its replicas actually have.
Illustrative trace (using the Complete Design assumptions, not a measurement): at the 100,000 redirects/sec design peak with a 92% hit ratio, the database sees 100,000 × 0.08 = 8,000 reads/sec. With Redis down, every redirect reaches it: 100,000 reads/sec, a 12.5× rise (1 ÷ (1 − 0.92)). Whether that is survivable is a headroom question — the figure only shows how the multiplier is derived.
User-visible impact
If the database has headroom, redirects continue working with higher latency. If it does not, the database itself becomes saturated and redirect latency/availability degrades broadly — this is the dangerous cascading-failure case: Redis fails, all traffic falls through, the database overloads, and the overload can then affect requests that would otherwise have succeeded.
Detection signals
Cache client error/timeout rate spikes, database read QPS and connection-pool saturation climb sharply, redirect p99 latency degrades.
Graceful degradation
A circuit breaker on the cache client that fails fast (rather than waiting out a slow TCP timeout) prevents each request from stalling on a dead cache before falling through. Request coalescing limits duplicate database reads for the same code. A process-local cache, if present, continues serving its hottest entries without touching Redis at all. Bounding database concurrency (a request budget or queue limit) protects the database from being driven past its own failure point even while absorbing the extra load.
Recovery
Redis restarts empty; it warms from real traffic as requests flow through it again. Uncontrolled instantaneous full fallback back onto a cold cache can itself cause a second stampede — a controlled warm-up/ramp (see Cold Cache / Cache Stampede below) is safer than assuming recovery is instantaneous.
Prevention / mitigation
Cache high-availability topology (replicated/clustered) where the added operational complexity is justified by the availability requirement; load shedding or rate limiting at the database boundary so a cache outage degrades gracefully rather than cascading; explicit, tested client timeout/circuit-breaker configuration rather than default library timeouts.
Database Primary Failure
The authoritative database's primary becomes unavailable. The effect is not uniform across the system — it depends on which operation and which cache state each request involves.
Trigger
Primary node crash, storage failure, or an unplanned/planned failover event.
Immediate effect by operation
Cache-hit redirects continue working normally — they never reach the database. Cache-miss redirects fail, because there is nowhere authoritative left to read from. New URL creation fails completely (no primary to write to). Custom-alias creation fails for the same reason. Delete/disable operations fail similarly.
User-visible impact
The fraction of redirects that continue working is exactly the fraction currently served from cache — this is workload-dependent, not a fixed percentage; a specific number (such as '92% of traffic') survives only if it reflects the actual, currently-measured cache hit ratio for this deployment. Any workload with a lower hit ratio, or a burst of first-time/expired lookups, sees proportionally more failed redirects.
Detection signals
Database connection errors on the write path and on cache-miss reads; primary health-check failures; replication-lag alerts on any surviving replica.
Graceful degradation
Serve an explicit, informative failure response (rather than a silent timeout) for operations that cannot succeed without the primary — including a Retry-After hint where appropriate. Cache-hit redirects require no special handling; they are already isolated from this failure.
Recovery
Promote a replica to primary and repoint the application's write target. How long this takes and how automated it is depends entirely on the database topology and failover tooling in use — there is no universal recovery-time window; it can range from seconds with a well-tested automated topology to much longer with manual intervention.
Prevention / mitigation
Automated, tested failover tooling appropriate to the chosen database and topology; replication configured with an explicit, understood consistency guarantee (synchronous, semi-synchronous, or asynchronous) rather than an assumed one; monitoring that distinguishes 'primary down' from 'replica lagging' so operators know exactly what has failed.
Hot Key in Redis
One short code receives a disproportionate share of all reads, concentrating load onto whichever single cache node/shard owns that key.
Trigger
A single URL becomes extremely popular relative to all others, and every one of its reads lands on the same cache key.
Immediate system effect
The node/shard owning that key approaches its own throughput ceiling handling just this one key, even while the rest of the cache cluster has plenty of spare capacity — clustering/sharding spreads different keys across nodes, but does not automatically spread one key's traffic.
User-visible impact
Elevated latency specifically for requests to this one code, potentially timeouts under extreme concentration, while requests for other codes remain unaffected.
Detection signals
Per-key or per-shard request-rate metrics (where available) showing one key or shard far above the rest; latency degradation isolated to one code rather than system-wide.
Graceful degradation / mitigation
A process-local cache in each app server absorbs the majority of reads for the hot key without touching the shared cache at all. Read replicas, where the cache topology supports them, let multiple nodes each serve the same key. Edge/CDN caching of the redirect response removes the request from the origin entirely once warm, for genuinely immutable, cacheable redirects. Request coalescing addresses the case where the key has just gone cold and would otherwise stampede.
Recovery
As traffic for the URL naturally declines, or once a mitigation (local cache, replica routing, edge caching) is in place, load on the single node/shard returns to normal.
Prevention
Proactively identify likely-to-go-viral content where possible and pre-warm a process-local cache; maintain per-key/per-shard observability so a hot key is detected before it degrades latency broadly.
Analytics Queue Backlog
The analytics event transport grows faster than consumers can drain it. This is graceful degradation, not an outage — as long as retention capacity is sufficient for the backlog that accumulates.
Trigger
Consumer throughput falls below producer (click-event) throughput — under-provisioned workers, a traffic spike, or a downstream analytics-store slowdown that backs up the whole pipeline.
Immediate system effect
Backlog age grows. The redirect path is unaffected as long as the producer-side policy (bounded timeout, non-blocking publish, drop/sample on failure) is respected — see Architecture Deep Dive → Analytics Delivery Mechanics.
User-visible impact
Analytics dashboards show stale click data. Redirects themselves are not affected.
Detection signals
Consumer group lag (age and/or event-count backlog), not just raw queue depth — backlog age matters more than raw event count for deciding urgency, since a large backlog of very recent events is a different situation from a smaller backlog that is very old relative to retention.
Graceful degradation
If retention/storage capacity comfortably covers the backlog, this is simply delayed analytics. If the backlog risks exceeding retention before being drained, older events are at risk of being dropped by the transport itself — at that point the system must choose explicitly whether to prioritize draining old events (preserve completeness) or fresh events (preserve currency), depending on product requirements.
Recovery
Scale out consumers to exceed the current producer rate plus enough headroom to drain the existing backlog in a reasonable window. Replaying a large backlog can itself add load to the downstream analytics store — pace the drain if the store cannot absorb a sudden full-speed replay.
Prevention
Autoscale consumers based on the lag metric itself (add capacity as lag grows, remove it once lag is low and stable); a fixed retention window (such as '24 hours by default') is not an architecture fact — retention is a configuration choice for the specific transport in use, and it should be set deliberately based on the acceptable backlog-drain window.
Cold Cache / Cache Stampede
Distinct from a total Redis outage: the cache is up, but a specific set of keys — or the whole cache — has just gone cold, and a large volume of requests arrives before the first miss has finished repopulating it.
Trigger
A cache restart, a newly deployed region with an empty cache, a popular key expiring, or a mass eviction under memory pressure — any event that clears cache state for one or more hot keys while traffic for them continues arriving.
Immediate system effect
Without coalescing, every concurrent request for the same now-cold code independently queries the database for the identical row, multiplying database load far beyond what the actual read volume would otherwise require — a database thundering herd.
User-visible impact
Elevated latency concentrated on the specific codes that just went cold, and — if the herd is large enough — potential database saturation that spills over into unrelated requests.
Detection signals
A spike in database query volume for a small set of distinct keys, correlated with a recent cache restart/deploy/eviction event, rather than a broad, uniform increase in traffic.
Graceful degradation / mitigation
Request coalescing/single-flight per key so only one database lookup happens per cold code regardless of concurrent request volume; a process-local cache continuing to serve any keys it already holds; bounding database concurrency so even an imperfectly-coalesced herd cannot fully saturate the database; a controlled recovery ramp (gradually reintroducing traffic to a freshly restarted cache) rather than assuming instant full-capacity fallback.
Recovery
As coalesced lookups complete and repopulate the cache, subsequent requests for the same keys become hits again — recovery is typically fast (seconds) for a small number of hot keys, longer for a fully cold cache warming from real traffic.
Prevention
Proactive cache warming for known-hot mappings ahead of a planned restart/deploy; jitter on TTLs so a large set of entries does not expire in the same instant, which would otherwise manufacture a synchronized stampede out of ordinary expiry.
Read Replica Lag
A URL is created successfully and committed on the primary, but a subsequent read lands on a replica that has not yet replayed that write — producing a false 'not found' for a mapping that genuinely exists.
Trigger
Cache population after creation fails, is skipped, or the entry is evicted before an app server needs to read it — and the resulting read is routed to an asynchronously-replicating replica that has not yet caught up to the primary's most recent commit.
Immediate system effect
The read replica correctly reports 'no such row' from its own current state, even though the mapping exists on the primary — the replica is not wrong given what it has replayed so far, it is simply behind.
User-visible impact
A just-created short link intermittently returns 404 for a brief window immediately after creation, which is a particularly bad experience since new links are often shared and clicked immediately.
Detection signals
Replication lag metric on the replica; a pattern of 404s correlated with very recent created_at timestamps once investigated.
Graceful degradation / mitigation
Populate the cache immediately after the primary commit succeeds, so most reads never reach a replica at all during the risk window. Route reads to the primary for a short window immediately following a write for that specific mapping ('read-after-write' routing). Use synchronous or semi-synchronous replication if the product genuinely cannot tolerate any staleness window. A short, bounded retry on an unexpected 404 for a very recently created mapping can also paper over a brief lag window, if acceptable.
Recovery
The replica catches up automatically as it continues replaying the write-ahead stream; no manual intervention is normally required for an isolated lag event.
Prevention
Monitor replication lag as a first-class metric; do not silently assume replication is synchronous — verify and document the actual guarantee the chosen topology provides. See Architecture Deep Dive → Database Access & Indexing and Trade-offs → Read from Primary vs Read Replica on Cache Miss.
Short-Code Collision / Namespace Pressure
Random code generation repeatedly produces already-occupied codes as namespace occupancy grows — a gradual pressure scenario, not a sudden outage.
Trigger
Namespace occupancy (the fraction of the keyspace already in use) rises over time as more mappings are created, increasing the per-attempt probability that a newly generated random code is already taken.
Immediate system effect
The database UNIQUE constraint prevents any actual data corruption — a collision on insert simply fails to insert and returns zero rows affected. The application detects this and retries with a new random code within a bounded retry budget.
User-visible impact
Under normal, low-occupancy conditions, negligible — most creations succeed on the first attempt. As occupancy rises, average retries-per-creation rises too, adding a small, growing amount of latency to the write path.
Detection signals
A collision-rate/retry-count metric on the creation path trending upward over time is the leading indicator — track this explicitly rather than discovering the problem only when retries start failing outright.
Graceful degradation
The bounded retry loop absorbs a reasonable number of collisions gracefully. If retries are repeatedly exhausted (which would only happen at very high occupancy relative to the keyspace), the creation request should fail explicitly rather than looping indefinitely, and the increasing exhaustion rate itself is the signal to act.
Recovery / mitigation
Increase code length before occupancy becomes operationally expensive — a longer code multiplies the keyspace and drives the per-attempt collision probability back down. This is a proactive capacity decision, not a reactive one; see Architecture Deep Dive → Short-Code Generation.
Custom Alias Race
Two clients request the same custom alias at nearly the same instant. Correctness here depends entirely on what actually arbitrates the conflict.
Trigger
Two concurrent creation requests specify the identical custom alias, arriving close enough in time that a naive check-then-act sequence would race.
The wrong approach
A pre-check such as 'SELECT to see if the alias is already taken, then INSERT if not' has a race condition: both requests can pass the SELECT before either has inserted, and both proceed to attempt the INSERT.
Correct behavior
The database's UNIQUE constraint on the short-code/alias column is the final, atomic arbiter. Both requests attempt the INSERT; the database guarantees exactly one succeeds, and the other receives a constraint-violation response that the application translates into an explicit conflict response to that caller.
User-visible impact
One caller receives their requested alias successfully; the other receives a clear conflict (e.g. 409) rather than silently failing, silently overwriting, or nondeterministically 'winning' based on request timing.
Detection signals
Constraint-violation rate on the creation path for alias requests specifically — a healthy signal, not an error to eliminate, as long as it correctly results in an explicit conflict response rather than a corrupted or duplicated mapping.
Prevention
Rely on the database's atomic uniqueness guarantee as the correctness mechanism, not application-level locking or pre-checks — this scales correctly under concurrency without additional coordination infrastructure.
Mapping Expiry vs Stale Cache
The authoritative mapping has passed its expiry, but a longer-lived cache entry (or stale local-cache state) keeps redirecting successfully — a correctness failure, not merely a performance issue.
Trigger
A cache entry was populated with a fixed TTL that was not bounded by the mapping's own expires_at, so the cache entry outlives the mapping's authoritative validity.
Immediate system effect
Requests for the expired short code continue to be served directly from cache — the authoritative store would correctly report it as expired, but the cache is never consulted because the entry is still 'valid' by the cache's own (incorrect) TTL.
User-visible impact
An expired link keeps working past its intended expiry, which can range from a minor inconsistency to a meaningful security/product problem depending on why the link was set to expire.
Detection signals
A discrepancy between database-reported expired mappings and continued redirect traffic for those same codes, if monitored; otherwise this tends to surface only when specifically tested or reported by a user.
Graceful degradation / prevention
Bound cache TTL by remaining mapping lifetime (cache_ttl = min(configured_cache_ttl, expires_at - now)), and where expiry metadata is cached alongside the mapping, verify it at serving time as a defensive guard rather than trusting cache presence alone. Background deletion of expired rows is storage hygiene — it is not, and must not be relied upon as, the mechanism that enforces expiry; the authoritative read path itself must treat an expired row as expired. See Architecture Deep Dive → Cache Expiry Correctness.
Recovery
Once the TTL bound is corrected, existing over-long cache entries eventually expire naturally; if immediate correction is required, explicitly invalidate cached entries for affected mappings rather than waiting out their (incorrect) TTL.
Analytics Broker Unavailable
The event transport (queue/broker) that carries click events becomes slow or fully unavailable. The system must have an explicit, pre-decided policy for this — not an accidental one discovered during the incident.
Trigger
Broker outage, network partition between app servers and the broker, or the broker itself becoming overloaded and slow to accept publishes.
Immediate system effect
Every publish attempt on the redirect path either blocks, times out, or fails — depending entirely on the producer configuration in place.
The failure mode to avoid
If the producer blocks or retries unboundedly waiting for the broker, redirect latency and eventually redirect availability degrade along with it — this must never be allowed to happen, since analytics is not supposed to be able to take down redirect serving.
Correct explicit behavior (choose one, deliberately)
Redirect anyway and drop or sample analytics events for the duration of the outage; buffer events locally within a strict, bounded size/time limit and flush once the broker recovers; or, if analytics loss is genuinely unacceptable for the product, accept a durable-retry policy with its associated stronger infrastructure and cost requirements. Whichever is chosen, it must be a bounded, tested behavior — not an emergent one.
User-visible impact
None on the redirect path, if the producer policy is correctly bounded and non-blocking. Analytics dashboards show a gap or reduced granularity for the outage window.
Detection signals
Publish error/timeout rate on the producer side; broker health metrics; analytics event-volume drop correlated with the outage window.
Recovery / prevention
Bounded publish timeouts and a non-blocking producer configuration verified under test, not just assumed from the client library's defaults. See Architecture Deep Dive → Analytics Delivery Mechanics.
Malicious Random-Code Scan / Abuse
An attacker requests huge numbers of random, mostly-nonexistent short codes — or creates URLs aggressively — connecting security directly to system scale rather than treating abuse only as a separate product feature.
Trigger
Automated scanning of the short-code namespace looking for valid/interesting mappings, or bulk automated creation of short URLs for spam/phishing purposes.
Immediate system effect without mitigation
A large volume of lookups for codes that do not exist bypasses any positive cache (nothing to cache-hit) and hits the database directly for every single request, unless negative caching is in place. Bulk creation similarly stresses the write path and consumes namespace.
User-visible impact
Degraded latency for legitimate traffic if the scan/abuse volume is large enough to compete for database or application capacity.
Detection signals
A high 404/not-found rate concentrated from a small number of sources or IP ranges; creation-rate anomalies per IP/account; negative-cache hit-rate spikes (if instrumented) indicating heavy scanning.
Graceful degradation / mitigation
Rate limiting per IP/account on both lookups and creation; negative caching of not-found results so repeated scans for nonexistent codes are absorbed by the cache rather than reaching the database each time; a creation quota to bound how many URLs a single actor can produce; WAF/abuse-control rules for clearly automated traffic patterns; a malware/phishing-disable flow so abusive destinations can be disabled after detection independent of the scanning defense.
Recovery
Once rate limiting/blocking takes effect, database and application load returns to baseline; disabled/flagged malicious mappings stop resolving via the disable flow rather than requiring deletion.
Prevention
Treat negative caching and rate limiting as standard parts of the architecture, not optional add-ons — see Architecture Deep Dive → Cache Hierarchy.