Short Code Generator

How do you generate millions of globally unique short codes without creating a distributed coordination bottleneck? This is one of the most common "goes deeper than expected" problems in system design. It appears simple on the surface — generate a random string — but at scale, every naive approach breaks down in a different way. This page works through each approach from first principles, explains why the obvious solutions fail, and arrives at the design used in TinyURL.

Concept Distributed Systems Uniqueness at Scale Concurrency Ongoing — needs more research
The Core Problem

Uniqueness without coordination

Any system that needs to generate unique identifiers at scale runs into the same fundamental tension: uniqueness requires knowing what already exists, but knowing what already exists requires coordination, and coordination is expensive.

In a URL shortener at 1,000 writes per second, the naive approach is obvious — generate a random 7-character string, check the database to see if it exists, and retry if it does. This works fine at low volume. At 1,000 writes/sec across multiple web servers, it breaks in several ways simultaneously.

First, every write now requires a round-trip database read before it can complete. The write path goes from "generate → insert" to "generate → read → if collision: generate → read again → insert." Under load, this adds latency to every single URL creation.

Second, there is a race condition at the heart of it. Server A generates code "abc1234" and checks the database — it is not there. Server B generates the same code "abc1234" and checks the database — it is not there either. Both servers now try to insert. One wins; the other gets a constraint violation and has to retry. At 1,000 writes/sec across multiple servers, collision handling under concurrency is not a theoretical concern — it is a regular occurrence.

Third, retry loops under high collision probability compound the problem. As the database fills up, collision probability grows. Even a small retry rate multiplied across 1,000 writes/sec generates meaningfully higher database load than expected.

What we are actually optimizing for

The goal is not just uniqueness — it is uniqueness without synchronous coordination on the hot path. "Hot path" means the code that runs on every single write request, in the critical section between receiving the request and returning a response. Every millisecond of hot-path latency is multiplied by your write RPS. Moving code generation off the hot path entirely is the design insight that makes everything else work.

Overview

Four approaches — tradeoffs at a glance

Before going deep on each approach, it helps to see how they compare on the dimensions that matter most. Each approach makes a different tradeoff between simplicity, uniqueness guarantee, hot-path latency, and coordination overhead.

ApproachUniqueness guaranteeHot path costCoordinationBest at scale
1 · Hash-basedProbabilistic + retry1+ DB reads per writeDB checkLow scale only
2 · Region prefixStructural (by prefix)Zero DB readsNoneMedium — burns code space
3 · Pre-gen poolGuaranteed (pre-claimed)One atomic DB claimMinimal (nanny)Medium-high — chosen ✓
4 · Token serviceGuaranteed (centralized)One service callToken serviceHighest scale
Approach 1

Hash-based generation

The most intuitive approach: take the long URL, run it through a hash function (MD5, SHA-256), take the first 7 characters of the base62 output, and use that as the short code. If it collides with an existing code, append a salt — the user ID, a counter, a timestamp — and try again.

The appeal of hashing is that the same long URL always produces the same prefix. This means you get natural deduplication — two requests to shorten the same URL will produce the same hash prefix, and you can return the existing code without creating a new one. At low volume, this works elegantly.

Hash + retry flow
Long URL input MD5 / SHA-256 first 7 base62 chars DB uniqueness check EXISTS? retry with salt hot path DB read Assign code if no collision retry with appended salt on collision
What works
  • Simple to implement and reason about
  • Same URL always hashes to same prefix — natural deduplication
  • No new infrastructure needed
  • Works well at low write volume
Why it breaks at scale
  • Every write requires a DB read on the hot path
  • Race condition: two servers check simultaneously, both see "not exists", both try to insert
  • Retry storms compound under concurrent load
  • Salt-based retries produce inconsistent codes for the same URL
The race condition in detail

Server A generates "abc1234" and checks the DB — not found. Server B generates "abc1234" and checks the DB — not found. Server A inserts "abc1234" — succeeds. Server B inserts "abc1234" — constraint violation. Server B must now generate a new code, do another DB read, and retry. At 1,000 writes/sec across multiple servers, this happens regularly. Each retry adds latency and additional DB load, which cascades under sustained write pressure.

Approach 2

Region prefix per server

The insight behind region prefix is elegant: if you can guarantee that no two servers ever produce the same prefix, you do not need to coordinate at all. Server US-1 generates codes starting with "u1", Server EU-1 generates codes starting with "e1". Two servers cannot collide if their codes are structurally distinct.

This completely eliminates the hot-path database read. A server generates a code locally in microseconds, inserts it, done. No coordination, no retry, no race condition. At pure write throughput, this is the fastest possible approach. The problem is what it costs.

What works
  • Zero coordination between servers
  • No DB read on hot write path
  • Scales horizontally without limit
  • Simple per-server implementation
Why it was abandoned
  • Burns code space: 2-char prefix leaves only 62^5 = 916M codes per server vs 62^7 = 3.5T
  • Codes leak infrastructure details — region and server identity visible in URL
  • Custom aliases have no prefix — breaks the model
  • Adding new servers requires new prefix registry
  • Easy to accidentally mix with pool approach — exactly what happened
Why this was abandoned in the TinyURL design

This approach was initially proposed and then accidentally mixed with the pre-generated pool approach — some logic was region-prefixing codes while other logic was pulling from a shared pool. The two approaches are fundamentally incompatible: region prefix assumes codes are generated on-demand with server identity encoded; the pool assumes codes are pre-generated centrally with no server-specific information. The lesson: pick one code generation strategy and commit to it end-to-end. Mixing approaches produces an inconsistent design that is harder to reason about than either pure approach.

Approach 3 — Chosen for TinyURL

Pre-generated code pool

The core insight of the pool approach is: move code generation entirely off the hot path. Instead of generating a code when a write request arrives, generate codes in advance and store them in a pool. When a write arrives, the web server claims the next available code from the pool. The pool stays full because a background service — the nanny — continuously refills it.

This separates two concerns that the hash approach conflated: generating unique codes (now done offline by the nanny, never blocking a user request) and assigning codes to URLs (now done on the hot path via an atomic database claim). The web server never generates a code — it only claims one that has already been guaranteed unique.

Pre-generated pool — separation of concerns
OFFLINE — happens in background Nanny Service generates random Base62 pool < 5K → generate batch Code Pool 10,000 codes ready UNIQUE constraint HOT PATH — per write request Write Request POST /shorten Atomic Claim DELETE + RETURN code guaranteed unique URLs Table insert + return web server reads next code Code generation happens here async, no user waiting Code assignment happens here fast, deterministic, no race condition
Why this works
  • No generation logic on the hot write path
  • No DB uniqueness check per request
  • Codes guaranteed unique at pool-creation time
  • Nanny runs async — never blocks user requests
  • Atomic claim prevents race conditions between servers
  • UNIQUE constraint catches theoretical pool duplicates
  • Reclamation service keeps pool sustainable indefinitely
Known limitations
  • Nanny downtime drains the pool (~10s before writes fail)
  • Multiple nanny instances need coordination
  • Pool table becomes contention point at very high scale
  • Atomic claim semantics vary across database engines
The atomic claim operation — why it eliminates the race condition

The web server claims a code with a single atomic operation: DELETE FROM code_pool WHERE code = (SELECT code FROM code_pool LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING code. The FOR UPDATE SKIP LOCKED clause means two servers can never claim the same code — one acquires the row lock and proceeds; the other skips that row and claims the next one. This is a single database operation, not a read-then-write pair. The atomicity is exactly what eliminates the race condition that breaks the hash approach.

Approach 4 — Future Direction

Distributed token service

At higher scale — 100,000+ writes per second, or a globally distributed deployment — the pool table itself becomes a bottleneck. Every write does an atomic DELETE on a shared table. A dedicated token service solves this by decentralizing code generation entirely.

Instead of all web servers competing for rows in a shared pool table, each server gets a pre-allocated range of IDs from the token service. Within its range, the server generates codes locally with zero coordination. When the range is exhausted, the server requests a new range — one network call per 1,000 codes instead of one per write.

Twitter's Snowflake is the canonical implementation: a 64-bit integer encoding a timestamp, datacenter ID, machine ID, and sequence number. Each server generates IDs entirely locally. Global uniqueness is guaranteed by the combination of time and machine identity — no central counter, no coordination at all.

Why not used in TinyURL at our stated scale

At 1,000 writes/sec, the pre-generated pool is comfortably sufficient — the pool table is not under meaningful contention. A token service adds a new piece of infrastructure to deploy, monitor, and maintain. That complexity is not justified unless the pool approach is demonstrably failing under load. Reach for a token service when the pool table is the measured bottleneck in production, not as a speculative upfront design choice.

Component Detail

Nanny service — the full design

The nanny service is deceptively simple. Its entire job is to keep the code pool full. It runs as a singleton background job, wakes up on a schedule, checks the pool size, and generates new codes if the pool is below threshold. The simplicity is intentional — a service with one clear responsibility is easy to reason about, monitor, and debug.

Why these specific numbers

Pool target: 10,000 codes. At 1,000 writes/sec, this is 10 seconds of write capacity. If the nanny runs every few seconds and takes a moment to generate codes, there is still plenty of buffer before the pool drains. If the pool target were only 1,000 codes (one second), a single nanny hiccup could drain the pool before it responds. 10,000 is conservative but the cost is negligible — the pool table at 10,000 entries of 10 bytes each is 100KB.

Refill threshold: 5,000 codes (50% capacity). The nanny does not wait until the pool is empty before refilling — that would leave no margin for error. Refilling at 50% means the remaining 5,000 codes provide approximately 5 seconds of additional runway even if the nanny takes longer than usual. This "buffer within a buffer" is what makes the system robust to nanny delays without requiring a high-frequency polling loop.

Nanny service pseudocode
// runs every N seconds
pool_size = SELECT COUNT(*) FROM code_pool

if pool_size < REFILL_THRESHOLD (5,000) {
  to_generate = TARGET_SIZE (10,000) - pool_size

  for i in range(to_generate) {
    code = random_base62(length=7)
    INSERT INTO code_pool (code)
    ON CONFLICT DO NOTHING  // safety net for duplicates
  }
}
The unique constraint — safety net, not crutch

The pool table has a UNIQUE constraint on the code column. At a pool size of 10,000 against a space of 3.5 trillion possible Base62 strings, the probability of a duplicate is approximately 0.0000003% — effectively zero. But production systems should never rely on probability when a deterministic check is cheap. The constraint catches the one-in-a-billion case, silently drops the duplicate (ON CONFLICT DO NOTHING), and the loop continues. No crash, no alert, no retry storm.

The nanny single point of failure — and what to do about it

If the nanny crashes and stays down, the pool drains at 1,000 codes/sec. With a 10,000-code buffer, writes start failing in roughly 10 seconds. Mitigations: (1) Monitor pool size as a critical metric — alert loudly at 2,000 codes (~2 seconds of runway). (2) Restart the nanny automatically on crash (systemd, Kubernetes restart policy). (3) Consider two nanny instances with a distributed lock — one is primary, one is standby. For a first production deployment, option 1 plus 2 is the right tradeoff: simple, fast to implement, catches the failure before it becomes a user-visible outage.

Component Detail

Code reclamation — the full lifecycle

Without reclamation, the code pool has a slow leak. Codes claimed for URLs that nobody ever clicks again are permanently consumed. At 1,000 writes/sec, approximately 86 million codes are claimed every day. Over time, even a 3.5 trillion code space shrinks. The reclamation service prevents this while solving a practical problem: codes from abandoned or deleted URLs should eventually be reusable.

URL state lifecycle — from active to reclaimed
ACTIVE receiving clicks state = ACTIVE no clicks 3 months INACTIVE DW batch job flags state = INACTIVE 1 more month SOFT DELETED state = DELETED record kept for audit reclaim service CODE POOL code re-inserted available for reuse
Why soft delete — not hard delete

The instinct when reclaiming a code is to delete the record entirely. But hard deletion has a dangerous consequence: if the short code is immediately reassigned to a new long URL, anyone who had cached or bookmarked the old short link would silently land on completely different content. This is a terrible user experience and a potential security issue.

Soft deletion — setting state = DELETED while keeping the record — means an expired link shows a clear "this URL has expired" page rather than silently redirecting to something unrelated. The record is also preserved for audit: if someone reports a link behaved unexpectedly, you have the full history of what the code pointed to and when it changed.

Why the grace period exists

The reclamation service waits one additional month after the INACTIVE state is set before moving to SOFT DELETED. This handles a real pattern: content that has a traffic lull but is not actually dead. A seasonal campaign that goes quiet in January might spike again in March. A blog post with irregular traffic should not lose its code during a quiet period. The 3-month inactivity window plus 1-month grace period gives 4 total months — long enough to correctly identify genuinely inactive URLs while preserving those with irregular but real traffic.

Needs More Research

Open questions — to be explored

The pre-generated pool approach as described is solid for TinyURL at its stated scale. But it has known gaps that become important at higher scale or in more complex deployments. These are left open intentionally — they require research and benchmarking rather than speculation.

  • Nanny high availability: Running two nanny instances without them generating overlapping codes requires either leader election (ZooKeeper, etcd) or range-based allocation per nanny instance. What is the right coordination primitive, and what is the operational overhead?
  • Atomic claim on non-relational databases: The DELETE ... FOR UPDATE SKIP LOCKED ... RETURNING pattern works on PostgreSQL. On Cassandra or DynamoDB, what is the equivalent? Conditional writes provide atomicity but with different performance characteristics.
  • Pool table as write bottleneck: At 10K+ writes/sec, is the pool table a contention point even with row-level locking? Could we batch-allocate codes to each web server — giving each a local in-memory buffer of 100 codes — to reduce pool table contention by 100x?
  • Custom alias conflicts with pool: If a user requests a custom alias that happens to exist in the pre-generated pool, the pool code must be invalidated. Does this need an explicit check, or does the UNIQUE constraint on the URLs table handle it cleanly?
  • Global multi-region deployment: Should each region have its own independent code pool, or a shared global pool? Independent pools eliminate cross-region coordination but risk producing the same code in two regions simultaneously.
  • Pool exhaustion recovery: If the pool drains to zero before the nanny refills it, what should the web server do? Fall back to inline generation? Queue the request? Return a 503? Each option has different user-experience implications.
This page will be updated as more research is done

Code generation uniqueness is a pattern that appears across URL shorteners, order ID systems, user ID systems, and event tracking. The open questions above apply across all these contexts. As more systems are designed and the answers become clearer, this page will be updated with concrete approaches and benchmarks.

Cross-reference

Systems that use this concept

The code generation problem — unique short identifiers at scale without coordination overhead — appears in many system designs. Each system has slightly different constraints but the core tradeoffs are the same.

Referenced from these designs
TinyURL — short codes Order ID System — coming soon Distributed Tracing IDs — coming soon User ID Generation — coming soon