How Unicode and UTF-8 work

How computers turn text into numbers and bytes, from ASCII to Unicode's code points and planes, how UTF-8 and UTF-16 encode them, why one visible character can be several code points, and why the web settled on UTF-8.

Published 9 min readThe web

A computer stores only numbers. To store text, it needs an agreement about which number stands for which character, and how those numbers are written as bytes. When two programs disagree, text turns into nonsense: the word “café” saved by one program can appear as “café” in another. This article explains the agreement almost all software uses today, Unicode, and the way of writing it as bytes that dominates the web, UTF-8, and shows exactly where “é” comes from.

From ASCII to Unicode

For decades, the basis was ASCII, a 7-bit code with 128 values for the English alphabet, digits, punctuation and control characters. In 1969, RFC 20 proposed it for the early ARPANET: “standard 7-bit ASCII embedded in an 8 bit byte whose high order bit is always 0.” The capital letter A is 65, or 41 in hexadecimal.

The eighth bit left room for 128 more characters, and many incompatible encodings filled it in different ways, one for Western European languages, others for Cyrillic, Greek or Hebrew, and multi-byte systems for Chinese, Japanese and Korean. The same byte meant different characters depending on the encoding, and a document could not easily mix scripts. The Encoding Standard, which defines how browsers handle text, still has to describe dozens of these legacy encodings.

Unicode replaced them with a single list. It gives every character a number, its code point, written as U+ followed by at least four hexadecimal digits: A is U+0041, é is U+00E9, the euro sign € is U+20AC. Unicode keeps ASCII’s numbers for the first 128 characters.

Code points and planes

The Unicode codespace runs from U+0000 to U+10FFFF: 1,114,112 code points. It is divided into 17 planes of 65,536 code points each:

  • Plane 0, the Basic Multilingual Plane (BMP), holds the common characters of all modern scripts. Most text uses nothing else.
  • Plane 1, the Supplementary Multilingual Plane, holds historic scripts, notations and large symbol sets, including emoji such as 😀 (U+1F600, GRINNING FACE).
  • Planes 2 and 3 hold additional, mostly rare, CJK ideographs.
  • Plane 14 holds special-purpose format characters, and planes 15 and 16 are for private use. All other planes are reserved.
Plane 0 · Basic Multilingual PlaneU+0000–U+FFFFA U+0041 · é U+00E9 · € U+20ACPlane 1 · Supplementary MultilingualU+10000–U+1FFFFgrinning face U+1F600Planes 2 and 3 · more ideographsU+20000–U+3FFFFPlanes 4 to 13 · reservedU+40000–U+DFFFFPlane 14 · special-purposeU+E0000–U+EFFFFPlanes 15 and 16 · private useU+F0000–U+10FFFF
Unicode's 1,114,112 code points in 17 planes. Almost all everyday text lives in plane 0.

Unicode 18.0, released in September 2026, encodes 172,808 characters. New versions add characters, but once a character is assigned, its code point cannot be changed and the character cannot be removed.

From code points to bytes

A code point is still just a number. To store or send it, it must be written as code units of a fixed size. Unicode defines three encoding forms, all able to represent every character:

Character Code point UTF-8 (bytes) UTF-16 (16-bit units) UTF-32 (32-bit unit)
A U+0041 41 0041 00000041
é U+00E9 C3 A9 00E9 000000E9
€ U+20AC E2 82 AC 20AC 000020AC
한 U+D55C ED 95 9C D55C 0000D55C
😀 U+1F600 F0 9F 98 80 D83D DE00 0001F600

UTF-32 is the simplest, one unit per code point, but uses four bytes even for A. UTF-16 uses one 16-bit unit for every character in the BMP and two for the rest. UTF-8 uses one to four bytes.

How UTF-8 works

UTF-8 was designed in September 1992 by Ken Thompson, following design criteria from Rob Pike, for the Plan 9 operating system, according to RFC 3629, which specifies it for the internet. Its rules fit in one table:

Code points Bytes Bit pattern
U+0000–U+007F 1 0xxxxxxx
U+0080–U+07FF 2 110xxxxx 10xxxxxx
U+0800–U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
U+10000–U+10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

The x positions are filled with the bits of the code point, from the right. For the euro sign, U+20AC needs three bytes, which have room for 16 bits:

U+20AC = 0010 0000 1010 1100001011100010E2000010100000108210110010101100AC1110: the first of three bytes10: a continuation byte
UTF-8 spreads the bits of a code point over several bytes. Fixed prefixes say which byte is which.

This simple scheme has valuable properties, listed in RFC 3629:

  • ASCII stays ASCII. Every ASCII character is the same single byte, and the bytes 00 to 7F never appear inside another character, so ASCII-based software keeps working.
  • The first byte gives the length. Its leading 1 bits count the bytes of the character, and every following byte starts with 10.
  • Boundaries are easy to find from anywhere in the text: a reader that lands in the middle of a character skips bytes starting with 10 until the next character begins.
  • Some bytes never appear: C0, C1 and F5 to FF. Text in another encoding is therefore unlikely to be valid UTF-8 by accident.
  • Sorting is preserved. Sorting UTF-8 strings byte by byte gives the same order as sorting by code point.

UTF-16 and surrogate pairs

UTF-16 descends from the earliest form of Unicode, which was designed as a fixed-width 16-bit encoding. To reach the other planes, Unicode set aside 2,048 code points, U+D800 to U+DFFF, called surrogates, which are never assigned to characters. A character outside the BMP is written as a surrogate pair: a high surrogate followed by a low surrogate, which together carry 20 bits.

U+1F600 − 10000 = F600as 20 bits, split in two halves0000111101= 3DD800 + 3DD83D1000000000= 200DC00 + 200DE00high surrogatelow surrogateUTF-16: D83D DE00
A surrogate pair: 20 bits split into two halves, each stored in a reserved range.

Because UTF-16 and UTF-32 use multi-byte units, their bytes can be stored in two orders. A byte order mark (BOM), the character U+FEFF at the start of a text, shows which order was used. UTF-8 has no byte order question.

One character, several code points

What a reader sees as one character is not always one code point. Many letters can be written in two ways: é is the single code point U+00E9, but also the letter e followed by U+0301 COMBINING ACUTE ACCENT. Both look identical, yet their bytes differ:

é as one code point    U+00E9          → UTF-8: C3 A9
é as e + accent        U+0065 U+0301   → UTF-8: 65 CC 81
One code pointéU+00E9UTF-8C3 A9Two code pointseU+0065accentU+0301UTF-865 CC 81NFCNFDBoth look like é.
Two code point sequences for one visible letter. Normalization picks one of them, so that equal text compares equal.

Unicode treats such sequences as equivalent and defines normalization forms to make them identical: NFC composes characters where possible, and NFD decomposes them. Programs that compare text can normalize it first, so that such invisible differences disappear.

Unicode calls what a user perceives as a single character a grapheme cluster: a base character together with the marks that belong to it. Counting code points therefore does not tell you how many characters a reader sees, and cutting a string between code points can split a letter from its accent.

Invalid UTF-8 is a security problem

Each code point has exactly one valid UTF-8 encoding. A sloppy decoder, however, might also accept an overlong form, such as C0 AF for the slash /, which really is 2F. RFC 3629 describes why this matters: a server that blocks the byte sequence for /../ in file paths, but whose decoder accepts the overlong C0 AE for the dot, can be tricked into reading files outside its folder. RFC 3629 notes that this exact exploit “has actually been used in a widespread virus attacking Web servers in 2001”. Decoders must therefore reject every invalid sequence instead of guessing.

UTF-8 on the web

The web has settled on UTF-8. The Encoding Standard, which browsers follow, states that “the UTF-8 encoding is the most appropriate encoding for interchange of Unicode”, requires it for new protocols and formats, and says that authors must use it.

It also explains the “café” from the beginning. Browsers treat the labels latin1, iso-8859-1, ascii and us-ascii all as the encoding windows-1252, which maps every byte to one character. If the UTF-8 bytes of é, C3 A9, are read as windows-1252, C3 becomes à and A9 becomes ©. Garbled text like this is a sign that it was decoded with a different encoding than it was written in.

Key points

  • Unicode gives every character a code point, from U+0000 to U+10FFFF, in 17 planes. Version 18.0 (2026) contains 172,808 characters.
  • An encoding form turns code points into code units: UTF-8 (1 to 4 bytes), UTF-16 (one or two 16-bit units, with surrogate pairs) or UTF-32 (one 32-bit unit).
  • UTF-8 keeps ASCII unchanged, marks every byte as a start or continuation byte, and is the required encoding for new formats on the web.
  • One visible character can be several code points; normalization (NFC, NFD) makes equal text compare equal, and grapheme clusters describe what readers see as one character.
  • Decoders must reject invalid and overlong UTF-8, which has been used to bypass security checks.

Sources

  1. The Unicode Standard, Version 18.0.0The Unicode Consortium, The Unicode Consortium, 2026Accessed
  2. UTF-8, a transformation format of ISO 10646 (RFC 3629)F. Yergeau, RFC Editor, 2003Accessed
  3. Encoding StandardWHATWGAccessed
  4. ASCII format for Network Interchange (RFC 20)V. G. Cerf, RFC Editor, 1969Accessed