UTF-8 vs UTF-16 vs UTF-32: what the bytes say
koboshi · Co-founder
Try it yourself
Everything in this article runs live in the converter. Paste your own text and watch every notation update at once.
Open the Unicode converterTake three short strings: Hello (five ASCII letters), café 中文 (an accented letter, a space, two CJK ideographs) and 😀 (U+1F600 GRINNING FACE, one emoji). Encode each string in UTF-8, UTF-16 and UTF-32, and you get this:
text UTF-8 UTF-16 UTF-32
Hello 5 bytes 10 bytes 20 bytes
café 中文 12 bytes 14 bytes 28 bytes
😀 4 bytes 4 bytes 4 bytesSame characters, same standard, wildly different byte counts. The rest of this article explains where those numbers come from and which encoding belongs where. You can reproduce every row in the Unicode converter: its UTF-8 and UTF-16 panels print the exact code units shown below.
Three encodings, one repertoire
UTF-8, UTF-16 and UTF-32 are not different character sets. Each is a Unicode transformation format: an algorithmic, reversible mapping from code points to sequences of code units. UTF-8 works in 8-bit code units (bytes), UTF-16 in 16-bit units, UTF-32 in 32-bit units. All three can represent the full codespace, U+0000 through U+10FFFF, and all three lose nothing. The only differences are unit size, byte count, and how the units serialize to bytes.
One term matters for everything that follows. A code point is the abstract number, like U+00E9 for é. A code unit is the storage chunk an encoding actually uses. In UTF-8 the letter é (U+00E9, LATIN SMALL LETTER E WITH ACUTE) is two code units, C3 A9. In UTF-16 it is one, 00E9. In UTF-32 it is also one, 000000E9. The code point never changes; only its packaging does. (The distinction between code points and the characters they name is covered in What is Unicode?.)
"Hello" UTF-8: 48 65 6C 6C 6F
UTF-16: 0048 0065 006C 006C 006F
UTF-32: 00000048 00000065 0000006C 0000006C 0000006F
"café 中文" UTF-8: 63 61 66 C3 A9 20 E4 B8 AD E6 96 87
UTF-16: 0063 0061 0066 00E9 0020 4E2D 6587
UTF-32: 00000063 00000061 00000066 000000E9 00000020 00004E2D 00006587
"😀" UTF-8: F0 9F 98 80
UTF-16: D83D DE00
UTF-32: 0001F600Why UTF-8 won the wire
UTF-8 is variable width: one byte for ASCII, two to four bytes for everything else. The leading byte announces the sequence length (a byte starting 110 means two bytes follow, and so on), and every trailing byte starts with 10, so a decoder can resync at any code unit boundary.
Its two decisive properties are practical, not theoretical. First, ASCII transparency: any ASCII text is already valid UTF-8, byte for byte, so decades of protocols, file formats and tools that treat bytes 0x00 to 0x7F as syntax (brackets, slashes, angle brackets) keep working untouched. Second, the WHATWG Encoding Standard, which browsers implement, requires UTF-8 for new protocols and formats and calls it "the mandatory encoding for all things". When the web platform mandates one encoding, that encoding wins. The mechanics of the byte patterns themselves are detailed in What is UTF-8?.
UTF-16, the in-memory incumbent
UTF-16 is also variable width, but with 16-bit code units. Every code point in the Basic Multilingual Plane (up to U+FFFF) is one unit, so 中 is 4E2D and 文 is 6587. Code points above U+FFFF need a surrogate pair: two units from the reserved ranges U+D800..U+DBFF (lead surrogates, also called high surrogates) and U+DC00..U+DFFF (trail surrogates, also low surrogates). For U+1F600, subtract 0x10000 to get 0xF600, split that into two 10-bit fields, and the formula yields the pair D83D DE00 you saw in the table.
UTF-16 is the string representation of the platforms most code runs on. ECMA-262 defines a JavaScript string as a sequence of 16-bit unsigned integer values, which in well-formed strings are UTF-16 code units; that is why "😀".length is 2, not 1. The Java Language Specification states that Java represents text as sequences of 16-bit UTF-16 code units. And Microsoft's own documentation describes UTF-16 as the native Unicode encoding of Windows. None of these platforms chose UTF-16 for the web; they adopted it in the early 1990s, when Unicode fit in 16 bits, and kept it for compatibility. The conversion math and the everyday traps are in What is UTF-16?.
UTF-32: fixed width that rarely pays off
UTF-32 uses one 32-bit code unit per code point, and the unit value equals the code point, so U+1F600 is simply 0001F600. The pitch is constant-time indexing: the nth code point sits at offset 4n. The catch is that software almost never wants the nth code point. Users think in grapheme clusters, and a cluster can span several code points (a base letter plus combining marks, or an emoji sequence), so fixed-width units do not give you fixed-width "characters". Meanwhile the storage cost is real: four bytes for everything, even though the codespace needs only 21 bits, which burns cache and bandwidth on large volumes of text. The Unicode Consortium's own FAQ makes the same point and notes a common compromise: store strings as UTF-8 or UTF-16, but pass single character values around as UTF-32, which is what character-property APIs and some internal processing pipelines do.
Endianness and the BOM
A UTF-16 code unit is two bytes and a UTF-32 unit is four, so writing one to a file requires choosing a byte order. Big-endian puts the most significant byte first: 中 becomes 4E 2D. Little-endian reverses it: 2D 4E. That is why the encoding labels UTF-16BE, UTF-16LE, UTF-32BE and UTF-32LE exist. The unmarked labels UTF-16 and UTF-32 instead allow a byte order mark at the start of the data: the character U+FEFF, whose serialized bytes identify the order (if no BOM is present, big-endian is the default). Read the first bytes and the encoding announces itself:
FE FF UTF-16, big-endian
FF FE UTF-16, little-endian
00 00 FE FF UTF-32, big-endian
FF FE 00 00 UTF-32, little-endian
EF BB BF UTF-8The trick works because U+FFFE, the byte-swapped twin, is a permanently reserved noncharacter: if a UTF-16 stream starts with FF FE, the receiver knows to swap every unit. UTF-8 escapes this entire discussion. It is defined as a sequence of bytes, so there is exactly one byte order and nothing to mark. The sequence EF BB BF (U+FEFF encoded in UTF-8) can appear at the start of a file, but it is only a signature saying "this is UTF-8", and many tools, from Unix shebang lines to JSON parsers, prefer it absent. Where a stream is already labeled UTF-16BE or UTF-16LE by a protocol, a BOM is neither necessary nor permitted; any U+FEFF there is content, a zero width no-break space.
UTF-8 dominates the network, UTF-16 dominates language runtimes and Windows, and UTF-32 mostly passes single code points through internal APIs. The bytes on disk and the units in memory are different decisions, and conflating them is where encoding bugs come from.
Rules of thumb
- Default to UTF-8, without a BOM, for files, protocols, databases and anything that crosses a network.
- Do not fight your runtime. In JavaScript, Java or C# code, strings are UTF-16; convert at the boundary, not inside the program.
- Reach for UTF-32 only for single code point values, such as property lookups, never as a bulk string format.
- When a byte count matters, say which count you mean: UTF-8 bytes, UTF-16 code units, code points, or grapheme clusters. All four differ for
café 中文and for any emoji.
To see the counts on your own text, paste it into the converter and compare the UTF-8 code units panel with the UTF-16 one. The characters panel will also name every code point, so you can check exactly which units belong to which character.
Try it yourself
Everything in this article runs live in the converter. Paste your own text and watch every notation update at once.
Open the Unicode converter