跪拜 Guibai
← Back to the summary

A Short Link Is an ID System Before It's a Redirect

The typhoon has arrived, and I can only stay at home this weekend. With nothing else to do, I'll write a blog post.

Also posted on my personal site: You thought short links were just Hash + 301/302?

You've probably seen links like this: when scrolling through X, a post ends with t.co/xxxx; when you get a delivery or promotional text, the body contains a string like t.cn/xxxx. They're just a few characters long, but clicking them accurately jumps to an article, a product page, or some absurdly long campaign URL.

This is a short link. It seems to simply turn a very long URL (Uniform Resource Locator) into a few characters and then redirect back to the original address when a user clicks—like a straightforward string manipulation problem.

You probably think: isn't this just hashing the long URL once, storing the mapping between the short and long links, and then returning a 301 or 302 when a user visits?

But once you actually start building, you'll run into several unavoidable problems one after another:

  1. Hash results are so long—how many characters should the short code actually be?
  2. What happens if two long links get the same short code?
  3. If you don't want to check for collisions every time, can you generate a unique short code directly?
  4. If the same short link is visited tens of thousands of times, do you query the database every time?
  5. Should the redirect return a 301 or a 302?
  6. As data grows, when do you actually need sharding?
  7. Once creation, redirection, and scaling are all in place, is the system ready to go live?

These aren't seven independent problems. The answer to one almost always pushes you into the next. Let's follow this chain down.

Problem 1: How many characters does the short code need?

Since we need to turn a long URL into a short code, the first question isn't which hash algorithm to pick, but how many bits of the output to keep.

Let's set a scale suitable for reasoning:

The number of links needed over ten years is:

100,000,000 × 365 × 10 = 365,000,000,000

That's 365 billion links.

Short codes typically use digits, lowercase letters, and uppercase letters—62 characters in total. This representation is called Base62. It's no different in principle from binary or hexadecimal; each position simply has 62 possibilities.

Length Representable Quantity
5 chars 916,132,832
6 chars 56,800,235,584
7 chars 3,521,614,606,208

6 characters can only hold about 56.8 billion—not enough. 7 characters provide 3.52 trillion combinations, which can cover the ten-year requirement under this assumption.

So "7 characters" isn't a convention or a number pulled from thin air. It's derived backwards from the character set, business scale, and lifecycle.

Of course, this is just the capacity ceiling. Custom short codes, reserved words, codes deleted and never reused, and collisions from random generation all eat into that space. But the first step is always the same: calculate capacity first, then decide length.

Length only answers "can we fit them all." Once we cram massive numbers of long URLs into a limited 7-character space, the next problem immediately arises: what if two links get the same short code?

Problem 2: How to handle collisions with truncated hashes

The most natural approach is to take an MD5 (Message-Digest Algorithm 5) hash of the long URL and then truncate the first 7 characters as the short code.

The idea sounds natural enough: the hash result is sufficiently random, and the same input will consistently produce the same output. The problem lies in "truncating the first 7 characters."

The MD5 we usually see is a 32-character hexadecimal string, using only characters 0-9 and a-f. If we directly truncate its first 7 characters, the encoding space isn't 62^7, but:

16^7 = 268,435,456

Only about 268 million combinations—not even enough for three days of new links.

Of course, you could first convert the MD5's 128-bit result to Base62, then truncate 7 characters. This brings the character space back to 62^7, but collisions still exist: two different long URLs can absolutely get the same 7-character prefix.

We need to distinguish two concepts here. The issue isn't "someone cracked MD5," but rather that we actively compressed a very large hash space into a very small short-code space. As long as writes continue, prefix duplication is a normal event.

Engineering typically handles this as follows:

  1. Create a unique database index on code.
  2. If a conflict occurs on insert, re-compute after salting the original URL, or regenerate a random code.
  3. Limit retry attempts and log collision metrics.

The unique index is the last line of defense. You can't rely on probability for uniqueness just because the hash "looks random."

Checking for duplicates and retrying works, but every short link creation requires verifying whether the short code is already taken. Since collisions come from "random mapping," can we generate a unique value directly from the source?

Problem 3: Where does the unique ID come from?

Yes. First assign an integer ID (Identifier) to each record, then convert that integer to Base62.

For example, using 0-9a-zA-Z as the character table, the decimal number 11157 converts to 2TX.

Below is a complete, runnable implementation.

base62.ts

const alphabet =
  '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
const base = 62n

export function encodeBase62(input: bigint): string {
  if (input < 0n) {
    throw new RangeError('input must be non-negative')
  }

  if (input === 0n) {
    return alphabet[0]
  }

  let value = input
  let result = ''

  while (value > 0n) {
    result = alphabet[Number(value % base)] + result
    value /= base
  }

  return result
}

console.log(encodeBase62(11157n)) // 2TX

This approach has three direct advantages:

But it also shifts the problem from "how to generate the short code" to "how to generate a globally unique ID."

Small scale: database auto-increment is enough

For internal tools or newly launched products, using the database's auto-increment primary key is the easiest. The database already handles concurrent allocation and uniqueness; you don't need to build a distributed ID generator first.

Its downside is also obvious: short codes are sequential and enumerable. Someone who gets 2TX can easily try the codes before and after it. If links shouldn't be traversable in order, this approach isn't suitable.

However, non-enumerability is not access control. Even with random short codes, truly private resources still need authentication, permission checks, or access tokens with expiration. Don't treat "hard to guess" as a security boundary.

When scale grows: batch allocation of ID ranges

When multiple servers create short links simultaneously, a central service can allocate a range of IDs at once. For example, node A gets 1-10000, node B gets 10001-20000. Nodes increment locally and request the next range when exhausted.

This maintains global uniqueness without needing to contact the central service for every single short link creation.

You can also use distributed IDs like Snowflake. However, Snowflake generates 64-bit integers, which can require up to 11 characters when fully converted to Base62. It solves high-concurrency unique IDs but doesn't guarantee short codes stay short.

None of these approaches is inherently superior:

Approach Pros Costs
Auto-increment ID + Base62 Simple, collision-free, short code Enumerable, relies on DB for ID allocation
Random Base62 Naturally parallel across nodes, doesn't expose order Requires unique index and collision retries
ID range + Base62 Reduces central service pressure Must handle range waste and node failures
Snowflake + Base62 Mature distributed generation Longer code, may still expose time information

If I were building a short link service just for internal team use, I'd pick database auto-increment first. If links must not be enumerable, I'd switch to random Base62 with the unique index as a safety net. Don't assume you need Snowflake just because system design diagrams include it.

The write-side problems are mostly solved here. But a short link is typically created once and clicked tens or hundreds of thousands of times. The system's main pressure quickly shifts from "how to generate" to "how to read."

Problem 4: Do you need to query the database for every redirect?

A short link service is a classic read-heavy, write-light system. If every click queries the database, popular short links will quickly concentrate read pressure onto the same record.

The full chain can be split into two paths:

flowchart LR
    subgraph Write[Create Short Link]
        A[Client] -->|POST long URL| B[Short Link Service]
        B --> C[ID Generator]
        C --> D[Base62 Encoder]
        D --> E[(Database)]
    end

    subgraph Read[Visit Short Link]
        F[Browser] -->|GET /2TX| G[Load Balancer]
        G --> H[Redirect Service]
        H --> I{Redis Hit?}
        I -->|Yes| J[Return Redirect]
        I -->|No| K[(Database)]
        K --> L[Write to Redis]
        L --> J
    end

The read path typically uses Cache-Aside, also known as look-aside caching:

  1. First query Redis with the short code.
  2. On a hit, return the target URL directly.
  3. On a miss, query the database and write the result back to Redis.
  4. When a link is modified or deleted, invalidate the corresponding cache.

There's one easily overlooked detail: non-existent short codes should also be cached briefly.

An attacker can continuously request random short codes. If every miss hits the database, the cache is effectively useless. Giving the "not found" result a short TTL (Time To Live) of a few dozen seconds can block a large volume of repeated invalid queries.

Caching solves "how to find the original address faster," but doesn't answer another question: the next time the browser clicks, should it go through the short link service again? This is precisely the difference between 301 and 302.

Problem 5: Should the redirect return 301 or 302?

301 and 302 are both HTTP (Hypertext Transfer Protocol) redirect status codes.

A short link can return 301. It indicates the resource has permanently moved to a new address. Browsers and intermediate caches can remember this result and may not even request the short link service on subsequent visits.

This does reduce service pressure, but it also means you lose some control.

Dimension 301 Permanent Redirect 302 Temporary Redirect
Browser caching More likely to be cached long-term Usually re-requests the short link service
Modifying target address Cached clients may continue visiting the old address Changes take effect more promptly
Click statistics Subsequent clicks may bypass the service Each visit typically goes through the service
Service pressure Lower Higher

If the short link target will never change and you don't care about every single click, 301 is very reasonable.

But most product-oriented short links need to modify targets, set expiration times, collect visit statistics, or even immediately block malicious addresses upon discovery. In these cases, I would default to returning 302 and set an appropriate Cache-Control based on business needs. If every click must reach the server, explicitly use no-store rather than relying solely on the browser's default behavior for 302.

For the standard semantics of status codes, you can directly consult MDN's documentation on 301 Moved Permanently and 302 Found.

Status codes aren't a memorization exercise. Choosing 301 or 302 essentially answers: which is more important for your product—performance, modifiability, or data statistics?

If you choose 302 and let every click return to the server, then read pressure won't be absorbed by browser caching. As traffic continues to grow, database capacity and throughput will eventually become the next problem.

Problem 6: When do you actually need sharding?

If we keep pushing the scale upward, the system will naturally progress to database replicas and sharding. But system design diagrams most easily create an illusion: as if a qualified short link service should have Redis, read replicas, sharding, and distributed IDs in its very first version.

In reality, a more normal evolution sequence is:

Phase 1: Get a single database running first

For internal tools and low-traffic products, this structure might be sufficient for many years.

Phase 2: Add caching when read pressure appears

Phase 3: Shard only when data volume truly exceeds single-database capacity

id % 3 is a very intuitive sharding example, but when expanding from 3 nodes to 4, the ownership of a large amount of data will change.

In real implementation, you can first map IDs to a fixed number of virtual buckets, then assign buckets to different databases. When scaling, migrate a portion of the buckets without needing to reshuffle all the data.

The key point remains: sharding is the answer to a capacity problem, not an admission ticket for a system design article. Without monitoring data proving that a single database has become the bottleneck, don't turn it into three databases yet.

At this point, creation, redirection, caching, and scaling all have answers. But just because a public service can run the core path doesn't mean it's ready to go live.

Problem 7: Is the core path working enough to go live?

Getting the short code generation and redirection path working only completes the core loop. A public service needs at least three more things:

  1. Abuse protection: Rate-limit creation, block phishing, malware, and dangerous protocols.
  2. Lifecycle management: Handle expiration, deletion, target address modification, and cache invalidation.
  3. Observability: Log creation success rate, collision count, cache hit rate, redirect latency, and invalid short code ratio.

Each of these could be its own article. I won't expand on them here, because they don't change the main thread of this piece: a short link is first an ID system, and second a high-frequency redirect service.

Finally

A short link isn't about truncating MD5, nor about implementing a distributed system architecture diagram as drawn.

First, calculate the short code length based on capacity, then choose an ID scheme based on whether enumeration is acceptable; let the database unique index guard correctness, use caching to absorb read traffic; finally, make a clear choice between 301 and 302 based on modifiability and analytics requirements.

When all these questions can be answered clearly, the system behind those 7 characters is truly designed.

Next time you build an internal sharing link, an invitation link, or a resource ID, consider starting with a Base62 encoder first, then gradually adding the parts that real traffic demands.

This article referenced ByteByteGo's How Does a URL Shortener Work? for its scale assumptions and foundational design.

(End)