← All case studies
System design teardown

Design a URL shortener.

How to build a service that turns long links into short ones and redirects billions of clicks reliably, with architecture diagrams, capacity math and a plan for what breaks.

Reference design, not a client projectHigh-level designRead-heavy: 100:1~40k redirects/s peak~11 min read

Overview

A URL shortener maps a long URL to a short code (dev.zy/aZ3k9Qx) and redirects anyone who opens it. The idea is simple. Doing it at internet scale is not: the service sits on the critical path of every click, so it has to stay fast, always available and hard to abuse, even when a single link goes viral.

This teardown walks through a design that handles billions of links and tens of thousands of redirects per second, and shows what happens when things go wrong.

THE ONE-LINE DESIGN

Reads dominate writes 100:1, so we make the redirect path a cache lookup (CDN, then Redis, then a sharded key-value store) and make code generation coordination-free with leased ID ranges, so neither path becomes a bottleneck.

Requirements

Functional

  • Create a short link from a long URL
  • Redirect a short link to its long URL
  • Optional custom alias and expiry date
  • Disable or delete a link
  • Click analytics (count, country, device, referrer)

Non-functional

  • Redirect availability 99.99%
  • Redirect latency p99 under 50 ms at origin
  • Durability: a created link is never lost
  • Scale: 100M new links per month, 100:1 read ratio
  • Codes are short and not guessable in sequence

Out of scope

Billing, team accounts, link previews and a full analytics dashboard UI. The design leaves room for them but does not depend on them.

Capacity estimation

Back-of-the-envelope numbers decide the architecture. The read/write split is what matters most here.

QuantityEstimateHow
New links~40/s avg, ~400/s peak100M / 30 days / 86,400 s, peak at 10x
Redirects~4,000/s avg, ~40,000/s peak100:1 read/write ratio, peak at 10x
Stored records (5 yrs)~6 billion100M x 12 months x 5 years
Storage~3 TB (about 9 TB replicated)6B records x ~500 bytes, RF=3
Hot cache~35 GB20% of one day's ~340M reads x 500 bytes
Code length7 characters62^6 is 57B (too tight); 62^7 is 3.5T (ample headroom)
Egress bandwidth~20 MB/s peak40k redirects/s x ~500 byte response

Nothing here is exotic: the working set fits in a handful of Redis nodes and the data fits in a small sharded store. The hard part is the latency and availability of the read path, not the volume.

API design

A small REST surface. Creation calls accept an Idempotency-Key header so a client retry never creates a second link.

EndpointPurposeNotes
POST /v1/urlsCreate a short linkBody: long_url, optional custom_alias, expires_at. Returns 201 with code and short_url; 409 if the alias is taken.
GET /{code}Redirect302 to the long URL; 404 if unknown; 410 if expired or disabled.
GET /v1/urls/{code}Link metadataOwner only.
PATCH /v1/urls/{code}Update expiry or disableTriggers cache and CDN invalidation.
DELETE /v1/urls/{code}Delete a linkSoft delete first, then purge after a grace period.
GET /v1/urls/{code}/statsClick analyticsAggregated from the analytics store, never from the hot path.

High-level architecture

ClientsBrowser · MobileAPI consumersCDN / Edgecache · WAF · DDoSAPI Gateway / LBTLS · auth · rate limitApplication tier · statelessRedirect Serviceread path · ~100x trafficShorten / Managecreate · alias · expire · deleteGETPOSTRedis Clustercode → URL · LRU + TTLnegative cacheSharded KV storeDynamoDB / CassandraRF=3 · hash(code) shardsRange Allocatoretcd / DB counterleases 1M-ID ranges1 · lookup2 · on missputlease rangeKafkaclick-events topicStream jobaggregate · dedupeClickHouseanalyticsasync eventLEGENDCompute (stateless)Data storeEdge / queueClient / neutral
Figure 1. Component view. The redirect path (top) only touches the CDN, Redis and the key-value store. Code generation (middle) never blocks on a shared counter. Analytics (bottom) is fully asynchronous.

Components

  • CDN / Edge: terminates TLS, absorbs viral spikes by caching redirects briefly, and runs the WAF and DDoS protection. Edge logs also feed analytics, so cache hits are still counted.
  • API Gateway / Load balancer: authentication for the management API, per-key and per-IP rate limiting, and routing of GET and POST traffic to separate pools.
  • Redirect Service: stateless and autoscaled on requests per second. It does one thing: resolve a code to a URL as fast as possible.
  • Shorten / Manage Service: validates URLs, generates codes, writes records, and handles alias, expiry and delete. It scales independently of redirects.
  • Redis Cluster: caches code to URL with an LRU policy and a TTL. It also stores negative results so unknown codes do not hit the database.
  • Sharded KV store: the source of truth (DynamoDB or Cassandra), partitioned by a hash of the code, replicated three ways.
  • Range Allocator: hands out blocks of one million IDs to Shorten instances (details below).
  • Kafka, stream job, ClickHouse: click events flow asynchronously into an analytics store so reporting never slows a redirect.

The redirect path

ClientbrowserCDN / EdgeRedirect SvcRedisKV storeKafkaeventsGET /aZ3k9Qx302 (edge cache hit)edge miss → originGET aZ3k9Qxlong URL (cache hit)get(code) — only on cache missrecord (url, expiry, status)SET code, TTL (backfill)click event (async, fire-and-forget)302 Location: long URL
Figure 2. A redirect. Most requests end at the CDN or Redis; the key-value store is only read on a cache miss, and the click event never blocks the response.
  1. The browser requests /aZ3k9Qx. The CDN answers from its edge cache when it can (short TTL, 30 to 60 seconds).
  2. On a miss the request reaches a Redirect Service instance, which checks Redis first.
  3. On a Redis miss it reads the key-value store, then backfills Redis with a TTL capped at the link's expiry.
  4. It publishes a click event to Kafka without waiting for an acknowledgement, then returns the redirect.

DECISION: 301 vs 302

A 301 (permanent) is cached by browsers, which cuts load but means we can never disable a link, change its target or count its clicks. The design uses 302 so every click reaches us (or our CDN) and control stays with us. The cost is more traffic, which the cache layers absorb.

Generating short codes

Shorten service instancesInstance Alocal range 1,000,000 - 1,999,999Instance Blocal range 2,000,000 - 2,999,999Instance Crange exhausted: lease nextRange Allocatoretcd / DB rownext_range += 1M(atomic CAS)lease rangePER-REQUEST PIPELINE: no coordination, no DB read to pick a codeNext IDlocal counterPermutekeyed FeistelBase620-9 a-z A-Z7-char codee.g. aZ3k9QxConditional PUTif_not_exists(code)Sharded KV storedurable · replicatedOn a conflict (custom aliasesonly) draw the next ID and retry.62^7 = 3.5 trillion codes.The permutation hides the sequence.
Figure 3. Coordination-free code generation. Instances lease ID ranges rarely; the per-request work is purely local.

The code generator has to produce unique, short and hard-to-guess codes without becoming a bottleneck. Options considered:

ApproachStrengthWeaknessVerdict
Hash the URL, take 7 charsStateless; same URL gives the same codeCollisions need a check and retry; leaks nothing but wastes writes as the table fillsRejected
Random 7 charsSimple, unguessableBirthday collisions grow with fill; every write needs a lookupRejected
Global counter to Base62Zero collisionsSingle hot counter; codes are sequential and enumerableRejected
Pre-generated key pool (KGS)No collisions at write timeExtra service to keep highly availableViable
Leased ranges + keyed permutation + Base62No collisions, no per-request coordination, unguessableRange leases are wasted if an instance crashesChosen

How the chosen approach works

  1. Each Shorten instance leases a block of 1,000,000 IDs from the Range Allocator (an atomic compare-and-swap on one row in etcd or a database) and renews at 20% remaining.
  2. For each request it takes the next local ID, so no network call is needed to pick a code.
  3. A keyed Feistel permutation scrambles the ID into another number in the same range, so consecutive links look unrelated.
  4. The result is Base62-encoded into 7 characters and written with a conditional put (if_not_exists).

A crashed instance wastes up to one million unused IDs. With 3.5 trillion possible codes and only ~6 billion used in five years, that waste is irrelevant. Region IDs are placed in the high bits of the ID, so two regions can never hand out the same code.

Custom aliases skip the generator: they are written with the same conditional put, and a conflict returns 409. Aliases are normalised, checked against a reserved-word list (api, admin, login) and length-limited.

Data model and storage

The dominant access pattern is a single-key lookup by code with no joins, which fits a key-value or wide-column store. Account and billing data lives separately in a relational database.

TableKeyFields
urlsPartition key: codelong_url, owner_id, created_at, expires_at, status (active, disabled, blocked), is_custom
owner_urls (index)owner_id + created_atcode: powers "list my links" without scanning
clicks (ClickHouse)Partition by day, order by code, tsevent_id, country, device, referrer_domain, is_bot
  • Sharding: partition by hash of the code, so writes and reads spread evenly. A managed store handles rebalancing.
  • Replication: three replicas per partition, quorum writes, with cross-region replication for disaster recovery.
  • Expiry: the store's native TTL removes expired rows. The Redirect Service still checks expires_at, so a link stops working on time even before the row is physically deleted.

Caching strategy

Because reads are 99% of traffic, caching is the design. There are three layers, each with a clear job:

LayerWhat it cachesTTL / policyProtects against
CDN / edgeRedirect responses30 to 60 sViral spikes and repeat clicks; keeps traffic off the origin
In-process LRUTop ~10k codes per instance10 to 30 sRedis hot keys and network hops
Redis clusterCode to URL (plus negative results)Hours; capped at link expiry; jitteredDatabase load; stampedes
  • Cache-aside with single-flight: when many requests miss on the same code at once, only one goes to the database and the rest wait for its result.
  • TTL jitter stops many keys from expiring at the same moment.
  • Negative caching (30 s) stops scanners and typos from hammering the database with unknown codes.
  • Invalidation: disabling or deleting a link publishes an invalidation event to every Redis and calls the CDN purge API. The short edge TTL is the backstop.

Scaling and multi-region

Global DNS / Anycastlatency-based routing · health checksRegion A (primary for writes)Edge + LBTLS · WAF · rate limitApp tierredirect + shortenRedis (regional)hot set · 95%+ hitsKV store replicasquorum reads / writesRegion BEdge + LBTLS · WAF · rate limitApp tierredirect + shortenRedis (regional)hot set · 95%+ hitsKV store replicasquorum reads / writesnearest regionnearest regionasync replicationRegions C to N follow the same shape. Any region can keep serving reads if the others are lost.
Figure 4. Each region can serve redirects on its own. Writes replicate asynchronously between regions.
  • Stateless tiers scale horizontally behind the load balancer, with autoscaling on requests per second and p99 latency.
  • Latency-based DNS / anycast sends each user to the nearest healthy region, and health checks remove a failed region within seconds.
  • Read-your-write across regions: a new link may not have replicated yet when someone in another region clicks it. On a local miss, the service does a quorum read against a peer region before returning 404, then negative-caches the answer for a short time.
  • Capacity headroom: each region is sized to absorb the traffic of a failed neighbour (N+1), so a regional outage is a capacity event, not an outage.

Analytics pipeline

Click analytics must never slow or break a redirect, so it is decoupled completely.

  • The Redirect Service emits a small event (code, timestamp, hashed and truncated IP, user agent, referrer) to Kafka using a non-blocking producer with a bounded local buffer. If Kafka is unavailable, events are dropped rather than delaying the redirect.
  • CDN access logs flow into the same topic, so edge-cache hits are counted too.
  • A stream job deduplicates by event_id, filters bots by user-agent and IP heuristics, and aggregates into ClickHouse (per code, per minute).
  • Stats endpoints read the aggregates, so dashboards cost nothing on the hot path.

The counts are at-least-once, then deduplicated: accurate enough for reporting, and explicitly not a billing-grade ledger.

Handling failures and edge cases

A design is only as good as its behaviour when things break. These are the situations it is built for:

SituationWhat happensHow the design handles it
A single link goes viral (hot key)1M requests/s for one codeCDN serves most traffic; in-process LRU and single-flight protect Redis and the database; hot keys can be replicated across cache shards.
Cache stampede after evictionMany requests miss at onceSingle-flight per key, jittered TTLs, negative caching.
Redis cluster unavailableCache layer lostFall back to the key-value store with a circuit breaker and load shedding; the store is provisioned to survive a fraction of full read traffic while Redis recovers.
Range Allocator downInstances cannot lease new rangesEach instance holds about 1M IDs, hours of buffer at peak; leases renew early; alerts fire long before exhaustion.
Instance crashes mid-rangeUnused IDs lostAccepted waste; the ID space is 500x larger than needed.
Custom alias collisionTwo users want the same aliasConditional put makes exactly one win; the loser gets 409.
Client retries a create requestDuplicate linksIdempotency-Key returns the original response.
Link expires or is deletedStale cached redirect410 Gone; cache TTL capped at expiry; explicit invalidation plus short CDN TTL.
Region outageOne region lostDNS fails over; other regions hold replicas and spare capacity; region bits prevent code collisions after failover.
Replication lagNew link 404s in another regionQuorum read against a peer region on a local miss before returning 404.
Kafka or analytics outageNo click eventsRedirects are unaffected; events buffer or drop; counts are backfilled from CDN logs.
Invalid or looping target URLBad data or redirect loopsValidate scheme (http/https), length (2,048 max) and normalise; refuse targets on our own domain.
Traffic surge or bot floodOrigin overloadWAF and rate limits per IP and key; autoscaling; load shedding of low-priority requests.

Security and abuse prevention

Shorteners are a favourite tool for phishing and malware, so abuse controls are part of the core design, not an afterthought.

  • Malicious targets: check every submitted URL against a reputation service (for example Google Safe Browsing) at creation, and re-scan periodically. Bad links are set to blocked and show a warning page instead of redirecting.
  • Enumeration resistance: keyed permutation makes codes non-sequential; 404 responses are uniform and rate-limited.
  • Rate limits on creation per API key and per IP; reserved words for aliases; HTTPS and HSTS everywhere.
  • Privacy: store only a truncated, hashed IP; support deleting all data for an owner on request.
  • Scanner isolation: any service that fetches a target URL runs in a sandbox with restricted egress, to prevent server-side request forgery.

Observability and SLOs

  • SLOs: redirect availability 99.99% (about 52 minutes of downtime a year), redirect p99 under 50 ms at origin, create availability 99.9%.
  • Golden signals per service: request rate, error rate, latency percentiles, saturation.
  • Cache health: hit ratio per layer, eviction rate, key-value store read latency and throttling.
  • Allocator health: remaining IDs per instance and lease latency, alerting well before exhaustion.
  • Pipeline health: Kafka consumer lag and dropped-event counts.
  • Synthetic probes create and resolve a link from several regions every minute, and page on failure.
  • Safe rollouts: canary deploys, feature flags, and rollback on SLO burn.

Trade-offs and alternatives

DecisionChosenAlternativeWhy
Redirect status302301Keeps control and analytics; cache layers absorb the extra load.
Primary storeWide-column / KV (DynamoDB, Cassandra)PostgreSQLPure key lookups at high scale; relational features are not needed. Postgres still holds accounts.
Code generationLeased ranges + permutationHash or randomNo collisions and no per-request coordination.
ConsistencyEventual across regionsStrong globalAvailability and latency matter more; fixed by quorum read on miss.
AnalyticsAsync, at-least-onceSynchronous countersRedirect latency must never depend on analytics.
Cache invalidationShort TTL + explicit purgeLong TTL onlyBounded staleness when a link is disabled.

Possible next steps

  • Bloom filters per region to reject unknown codes without a database read.
  • Per-link QR codes and deep links for mobile apps.
  • Team workspaces, custom domains and branded short links.
  • A read-through edge worker so redirects resolve entirely at the CDN.

Facing a similar problem?

Tell us what's going on and we'll get back to you within one business day.