What you'll learn
Quick Answer
A URL shortener stores your long URL next to an auto-incrementing integer ID, encodes that integer into a short string using Base62 (0-9, A-Z, a-z — all safe in a URL path), and hands you back short.link/. Visiting it just decodes the string back into the integer, looks up the row, and issues an HTTP redirect. We wrote and ran the actual encoder and decoder below.
It's Just an Integer Wearing a Disguise
Most real URL shorteners don't generate a random short code first and then figure out where to store your long URL. They do it backwards, and the backwards way is the clever part: they insert your long URL into a database as a normal row, let the database hand out the next auto-incrementing primary key — 1, 2, 3, and so on — and then translate that integer into a short string on the way out to you. The short code isn't compressed text and it isn't random. It's a plain row ID, wearing a disguise that happens to look good in a URL.
This design is elegant for a reason that's easy to miss: uniqueness comes for free. You never have to check whether a code is already taken, because you're not generating a code and hoping it's free — you're translating an ID the database already guaranteed was unique. Compare that to the alternative some shorteners use instead — generate a random string first, then query the database to check if it collides with an existing one, and retry if it does — which works, but adds a lookup-and-retry loop that the ID-translation approach skips entirely. The trade-off is real too: a directly-encoded sequential ID is guessable and enumerable (anyone can try incrementing the decoded ID and walk through every link ever created), which is exactly why production systems that care about that either scramble the ID before encoding it or fall back to the random-with-collision-check approach instead.
Why Base62, Specifically
Encoding a number in a larger base means each character in the result can represent more possible values, so you need fewer characters to represent the same number. That's the whole motivation — it's exactly the same idea as why writing 255 as hexadecimal FF is shorter than writing it as decimal 255, just taken further. Base62 pushes that idea about as far as it can go using only characters that are always safe, unreserved, and unambiguous inside a URL path.
The alphabet is the ten digits 0-9, the twenty-six uppercase letters A-Z, and the twenty-six lowercase letters a-z — 10 + 26 + 26 = 62 symbols, hence the name. Every one of those characters can sit in a URL path segment without needing any escaping or special handling, unlike Base64's alphabet, which we'll get to below. Case is preserved and treated as meaningfully different too (unlike a domain name, where case is normalized and ignored), so aB3 and Ab3 are two completely different, valid short codes.
The Actual Encoder and Decoder, Run for Real
Here's real, complete Base62 encode and decode logic in Python. Encoding repeatedly divides the number by 62 and reads off the remainder as a symbol from the alphabet, building the string from the last digit to the first (exactly the same process you'd use by hand to convert any number into any base). Decoding reverses it: walk the string left to right, and for each character, multiply the running total by 62 and add that character's position in the alphabet.
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
BASE = 62
def encode(num: int) -> str:
if num == 0:
return ALPHABET[0]
digits = []
n = num
while n > 0:
n, rem = divmod(n, BASE)
digits.append(ALPHABET[rem])
return "".join(reversed(digits))
def decode(s: str) -> int:
num = 0
for ch in s:
num = num * BASE + ALPHABET.index(ch)
return num
test_ids = [0, 1, 9, 61, 62, 63, 125, 1000, 999999, 56800235583, 3521614606208]
print("id -> base62 -> decoded -> round_trip_ok")
for i in test_ids:
enc = encode(i)
dec = decode(enc)
print(f"{i:>15} -> {enc:>10} -> {dec:>15} -> {dec == i}")And the actual, real console output from running that exact code:
id -> base62 -> decoded -> round_trip_ok
0 -> 0 -> 0 -> True
1 -> 1 -> 1 -> True
9 -> 9 -> 9 -> True
61 -> z -> 61 -> True
62 -> 10 -> 62 -> True
63 -> 11 -> 63 -> True
125 -> 21 -> 125 -> True
1000 -> G8 -> 1000 -> True
999999 -> 4C91 -> 999999 -> True
56800235583 -> zzzzzz -> 56800235583 -> True
3521614606208 -> 10000000 -> 3521614606208 -> TrueEvery one of these round-trips cleanly, including the edge cases: 0 encodes to the single character '0' rather than an empty string, and 61 — the largest value that fits in one character — encodes to 'z', the last symbol in the alphabet. At 62 it rolls over to two characters ('10'), exactly the way decimal rolls from 9 to 10. The value 56,800,235,583 is 62^6 - 1, the largest number representable in exactly six Base62 characters, and it encodes to six z's — the base62 equivalent of decimal rolling over from 999999 to 1000000. One character more (62^7, or 3,521,614,606,208) encodes to '10000000', an eight-character string, the same way 10^7 in decimal is an eight-character string. Six Base62 characters alone cover over 56.8 billion distinct IDs — comfortably past what almost any URL shortener will ever need to hand out.
Why Not Just Use Base64 Directly?
Base64 is the more familiar encoding, and it's tempting to reach for Python's built-in base64 module instead of writing your own Base62 functions. Here's why that doesn't work cleanly for a URL path — shown with the real output of actually encoding the same numbers with it:
import base64
for n in [0, 1, 62, 1000, 999999, 56800235583, 3521614606208, 999999999999]:
length = max(1, (n.bit_length() + 7) // 8)
encoded = base64.b64encode(n.to_bytes(length, byteorder="big")).decode("ascii")
print(f"{n:>15} -> b64 {encoded!r}") 0 -> b64 'AA=='
1 -> b64 'AQ=='
62 -> b64 'Pg=='
1000 -> b64 'A+g='
999999 -> b64 'D0I/'
56800235583 -> b64 'DTmO0D8='
3521614606208 -> b64 'AzPwlm+A'
999999999999 -> b64 '6NSlD/8='Look at what shows up: +, /, and =. Every one of those is a real problem specifically inside a URL path, not a cosmetic issue. A / is the path separator itself — one embedded inside what's supposed to be a single opaque short code would silently turn short.link/DTmO0D8= into what looks like extra path segments to a router. A + has historically meant a literal space in form-encoded query strings, which creates exactly the kind of ambiguity you don't want in something meant to be pasted around unmodified. And = is Base64's padding character, doing nothing useful in a URL while also being the character that separates keys from values in a query string. Base64 does have a URL-safe variant — swapping +// for -/_ and usually dropping the = padding — precisely to patch this problem after the fact. Base62 sidesteps needing a patch at all: it's built only from characters that were never going to cause a problem in a URL path in the first place, so there's no substitution table and no padding to strip.
The Redirect Itself: Small Logic, Big Scale Problem
The read path is the mirror image of encoding: parse the short code out of the requested path, run decode() on it to get back the original integer ID, look up that ID's row in a database, and respond with an HTTP redirect to the stored long URL. There's a genuine, easy-to-miss product decision buried in which redirect status code you use. A 301 (permanent redirect) gets cached by the visitor's browser, which means repeat clicks on the same link never even reach your server again — great for your infrastructure load, but it also means you lose the ability to change where that link points later, and you lose visibility into every subsequent click. A 302 (temporary redirect) forces every single click back through your server, which costs more at scale but is what most commercial shorteners actually use, because click analytics — counting how many times a link was opened, from where, on what device — is usually the actual business model.
The genuinely hard scaling problem in a URL shortener was never the Base62 math — encoding and decoding an ID is a handful of divisions and array lookups, trivially fast at any scale. The hard part is the lookup on every single redirect. This is about as close to an ideal caching problem as exists in backend engineering: the workload is extremely read-heavy, latency genuinely matters because a redirect is sitting directly on someone's critical path (a link they're waiting on, often from a text message or a QR code they just scanned), and the id-to-URL mapping essentially never changes once it's written. That's exactly the profile a cache in front of the real database (Redis, Memcached) is built for, and it's why a serious URL shortener's infrastructure diagram is mostly about that cache layer, not about the encoding scheme.
Custom Aliases and Why They Break the Clean Design
The sequential-ID trick only works cleanly for auto-generated codes. The moment a product wants to let someone claim short.link/priodemy as a vanity alias, that string doesn't correspond to any integer at all — it's an arbitrary value someone typed in. Real systems handle this by keeping the short code as its own separate, indexed lookup column, rather than something always derived mathematically from an ID. The Base62-encoded auto-ID path is only the default taken when nobody asked for something specific; a custom alias has to go through an actual collision check against existing codes, the same check the ID-derived path was specifically designed to avoid.
It's also worth noting most shorteners don't bother reclaiming or expiring old codes for reuse, even though the ID space is finite in theory. Reusing a short code for a different destination later is a trust and security problem on its own — an old bookmark, printed flyer, or years-old text message pointing at content that's since changed hands is a worse outcome than just letting the ID space grow. And given how far even a handful of Base62 characters stretches — six characters alone cover over 56.8 billion values, as shown above — there's rarely a practical reason to reclaim anything.
