UTF-8 vs Base64 explained
August 11, 2026 · 13 min read
UTF-8 is a character encoding: it maps Unicode code points to bytes so computers store and transmit human-readable text. Base64 is a binary-to-text encoding: it maps arbitrary bytes to a limited ASCII alphabet. They solve different problems and are often used together - UTF-8 first, then Base64 when binary-safe text is required.
Confusing the two leads to classic bugs: “double encoding,” mojibake after decode, and APIs that return Base64 when clients expected UTF-8 strings.
Different jobs
UTF-8 variable-length sequences represent characters. Emoji, Latin, and CJK all share one byte stream format. Base64 does not represent characters - it represents bytes as 64 printable symbols. Decoding Base64 always yields bytes; whether those bytes are UTF-8 text, PNG image data, or AES ciphertext depends on context.
Text "café" → UTF-8 bytes: 63 61 66 C3 A9
Those bytes → Base64: Y2Fmw6k=
How they stack
Correct order for sending Unicode text in JSON as Base64: string → UTF-8 bytes → Base64 encode. Reverse on receive: Base64 decode → UTF-8 bytes → string. Skipping the UTF-8 step and Base64-encoding JavaScript strings with btoa breaks for code points above U+00FF.
const utf8Bytes = new TextEncoder().encode("café");
const b64 = btoa(String.fromCharCode(...utf8Bytes));
const back = new TextDecoder().decode(
Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))
);
Common mistakes
Treating Base64 output as if it were UTF-8 text produces nonsense when displayed directly. Interpreting UTF-8 bytes as Latin-1 after decode shows accented characters wrong. “Fixing” garbled text by re-encoding without identifying which layer failed makes recovery harder.
- Calling
btoaon Unicode without UTF-8 conversion first. - Using
TextDecoder('utf-8', { fatal: false })on random binary and silently accepting replacement characters. - Storing Base64 in databases when a
BLOBor bytea column would be clearer.
APIs and JSON
JSON is UTF-8 on the wire. String values are Unicode text, not Base64, unless your schema says otherwise. Fields named data or content are often Base64-wrapped binary - document which fields are which in OpenAPI or JSON Schema.
Protobuf and gRPC use bytes types; REST wrappers sometimes Base64 those for JSON clients. Keep type metadata (contentType, encoding) adjacent to encoded fields.
Debugging garbled text
When text looks wrong after decode, inspect hex of the byte layer: valid UTF-8 starts with predictable patterns; Latin-1 misread UTF-8 shows C3 prefixes as Ã. A local converter that shows hex and UTF-8 side by side speeds diagnosis without sending data to a server.
If hex matches known file signatures (PNG 89 50, gzip 1F 8B), you are handling binary - not text - and should not force UTF-8 interpretation.
FAQ
- Is UTF-8 the same as Base64?
- No. UTF-8 encodes characters to bytes; Base64 encodes bytes to a text-safe string. They are unrelated algorithms.
- Which comes first, UTF-8 or Base64?
- UTF-8 when your source is text. Base64 is the outer layer for transport in text-only channels.
- Why does btoa fail on emoji?
- btoa expects Latin-1 code units. Encode to UTF-8 bytes first, then Base64 those bytes.
- Can Base64 represent any UTF-8 string?
- Yes, by encoding the UTF-8 bytes of the string. The size grows versus raw UTF-8 in JSON.
Related: How to decode Base64 safely