Developer FIELD NOTES · 4 MIN READ
Punycode Explained: How International Domains Fit into ASCII
Why münchen.de is really xn--mnchen-3ya: how Punycode encodes non-ASCII domain labels, what xn-- means, and how to decode it back.
The domain name system is older than the web and stricter than your keyboard: a domain label may contain only letters, digits, and hyphens — ASCII, nothing else. That is why typing münchen.de works even though no resolver anywhere has ever stored an ü. Behind the scenes the label is rewritten as xn--mnchen-3ya, and Punycode is the algorithm doing the rewrite. Understanding it explains international domains, browser address-bar behavior, and a whole family of look-alike tricks.
The problem: DNS predates every accent mark
When DNS was designed, a "label" meant the 26 English letters, ten digits, and the hyphen — the so-called LDH characters. Sixty-three bytes per label, no spaces, no accents, and certainly no Japanese kana. When the internet went global, the choice was stark: replace DNS (impossible) or translate every non-ASCII name into LDH-compatible form on the way in and back on the way out. That translation layer is IDNA, Internationalized Domain Names in Applications, and Punycode is its encoding. The same characters-versus-numbers story underlies all text handling — How Text Encoding Works tells it from the top.
How the encoding squeezes ü into three characters
Punycode works label by label. It keeps the characters that are already legal — for münchen, that is mnchen — deletes the ones that are not, and then appends the deleted information as a compact digit string after a delimiter. The result is mnchen-3ya: mnchen is the ASCII skeleton, the final - announces "encoded characters follow", and 3ya packs two facts into three characters — how many characters were removed (one) and which one (ü, plus where it belongs). The digit alphabet runs a through z for values 0–25 and 0 through 9 for 26–35, which is why the suffix always looks like scrambled lowercase. Full-domain example:
münchen.de → xn--mnchen-3ya.de
bücher.example.com → xn--bcher-kva.example.com
例え.テスト → xn--r8jz45g.xn--zckzah
Note what did not change: .de, example.com — anything already ASCII passes through untouched, because per-label encoding means normal domains pay nothing.
Why the prefix is xn--
xn-- is a reserved flag meaning "this label is encoded; decode me before use". Regular labels are not allowed to contain a hyphen in both the third and fourth positions, so xn-- can never collide with a genuine name — the prefix is unambiguous by construction. Your browser decodes it and displays münchen.de in the address bar; the wire, the cache, and the resolver only ever see the ASCII form. Both directions matter in practice: registering an international domain means finding its Punycode form, and reading a certificate, log line, or email header often means decoding it back.
The look-alike problem is built in
Because any Unicode character can hide inside an xn-- label, domains can be crafted where a Cyrillic а stands in for a Latin a, or two labels render identically but decode to entirely different text. Browsers mitigate this — displaying Punycode when a label mixes scripts that look alike — but the underlying ambiguity is real, and it is worth decoding any xn-- label you do not recognize before trusting where it leads. Seeing the decoded form is the difference between "looks like your bank" and "is spelled with three characters your bank would never use".
Punycode is not URL encoding
Two encodings, two layers, endlessly confused. Percent-encoding (the %20 in URLs) encodes characters that are forbidden in a URL path or query string; Punycode encodes characters that are forbidden in a domain label. A path can carry an ü in percent-encoded form — %C3%BC — while the domain must carry it as xn-- punycode, and neither mechanism can substitute for the other. If you are debugging a full URL, both layers can be present at once, in different parts of the same string.
Try it yourself
Type münchen.de into the Punycode Converter and confirm the xn--mnchen-3ya output, then flip to decode mode and paste the result to get the umlaut back. To see the percent-encoding that handles the rest of a URL, try the URL Encoder, and to look up the exact code points behind the non-ASCII characters you are encoding, the Unicode Lookup maps each one to its number.