Base64 is a way of writing any bytes, text or binary, using only 64 plain characters, so that the data survives systems that only trust text: e-mail, JSON, URLs, HTML attributes and configuration files. It is not encryption and gives no secrecy at all, it makes the data about 33% larger, and it has a few traps: the URL-safe variant, the = padding, and the way text must be turned into UTF-8 bytes first. This article shows how the encoding works with a hand-checked example, the traps with real values you can paste into a Base64 tool, and when a data URI is a good idea and when it is not.
How it works
Base64 reads the data three bytes at a time, which is 24 bits, and cuts those 24 bits into four groups of 6 bits. Each 6-bit group is a number from 0 to 63, and each number is written with one character from this alphabet: A to Z are 0 to 25, a to z are 26 to 51, 0 to 9 are 52 to 61, + is 62 and / is 63.
A worked example with the three letters Man:
| Step | Value |
|---|---|
| Bytes (ASCII) | M = 77, a = 97, n = 110 |
| As 8-bit binary | 01001101 01100001 01101110 |
| Regrouped in 6 bits | 010011 010110 000101 101110 |
| As numbers | 19, 22, 5, 46 |
| As characters | T, W, F, u |
So Man is TWFu. Three bytes always become four characters.
Padding
When the data does not divide by three, the last group is short. Base64 fills the output with = so that its length is a multiple of four:
Man(3 bytes) isTWFu: no padding.Ma(2 bytes) isTWE=: one=.M(1 byte) isTQ==: two=.
The padding carries no information, because the length already says how many bytes are missing. That is why a decoder can accept text without it, and why many systems, JWTs among them, leave it out. A = is only ever valid at the very end.
It is not encryption
Anyone can decode Base64, with no key and no effort. A web request that carries Authorization: Basic dXNlcjpwYXNz is sending the text user:pass, written in Base64. That is why Basic authentication must only travel over HTTPS. A Kubernetes Secret stores its values in Base64 too, which is a way to hold binary values in a text file, not a way to protect them.
If you need to hide something, use real encryption. If you need to check that data was not changed, use a hash instead; verifying a download with SHA-256 shows how, and the output there is hexadecimal, not Base64. Encoding is reversible by design. A hash is not.
The size cost
Every 3 bytes become 4 characters, so the output is about a third larger: 33.3%, plus up to two = characters at the end.
| Input size | Base64 length | Growth |
|---|---|---|
| 3 bytes | 4 characters | 33% |
| 100 bytes | 136 characters | 36% |
| 1,000 bytes | 1,336 characters | 33.6% |
| 3,000 bytes | 4,000 characters | 33.3% |
| 1,000,000 bytes | 1,333,336 characters | 33.3% |
The formula is 4 × the ceiling of (bytes ÷ 3). Breaking the output into lines of 64 characters, as in PEM certificates, or 76, as in e-mail attachments, adds a line break per line on top.
The UTF-8 trap
Base64 encodes bytes, not letters. Text must first be turned into bytes, and the choice of encoding matters. On the web it is UTF-8. The word café is the five bytes 63 61 66 c3 a9 in UTF-8, so its Base64 is Y2Fmw6k=.
JavaScript's old btoa function treats each character as one byte, so for café it gives Y2Fm6Q==. Another system that decodes that as UTF-8 sees caf followed by an invalid byte. For Việt it does not even try: btoa("Việt") throws an InvalidCharacterError, and so it does for any emoji. The correct way in a browser is to encode the text to UTF-8 bytes first:
const bytes = new TextEncoder().encode("Việt");
const base64 = btoa(String.fromCharCode(...bytes)); // "Vmnhu4d0"
const back = new TextDecoder().decode(
Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)),
); // "Việt"Spreading the bytes into String.fromCharCode is fine for short text; for large data, convert in chunks or read a file with FileReader, because a huge argument list can overflow the call stack. The longer sample Hello, Việt Nam! 😀 café encodes to SGVsbG8sIFZp4buHdCBOYW0hIPCfmIAgY2Fmw6k= and decodes back unchanged. On a command line the bytes are whatever your terminal sends, normally UTF-8:
printf 'café' | base64 # Y2Fmw6k=
printf 'Y2Fmw6k=' | base64 -d # caféUse -D instead of -d on older versions of macOS. GNU base64 wraps its output at 76 characters unless you add -w 0. In PowerShell: [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("café")).
If a decoded text shows odd characters such as é where é should be, the text was encoded as UTF-8 and read as Latin-1, or the other way round. The encoding is the problem, not the Base64.
URL-safe Base64
The standard alphabet has + and /, and both mean something in a URL: + can be read as a space in a query string, and / is a path separator. The URL-safe variant, called Base64URL, writes - instead of + and _ instead of /, and usually drops the = padding. The four bytes fb ff fe ff are +//+/w== in standard Base64 and -__-_w in URL-safe Base64 without padding. Use the URL-safe form for anything that goes into a URL, a file name or a token. The header {"alg":"HS256","typ":"JWT"} is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, which happens to need no padding; see how to read a JWT for where Base64URL is used in practice.
Data URIs
A data URI puts a file inside a text value: data:image/png;base64, followed by the Base64 of the file. The smallest valid PNG, a 1 by 1 transparent picture, is 70 bytes and becomes 96 characters:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==You can use that as the src of an <img>, or in a CSS url(). The benefit is one request fewer, which can matter for a tiny icon. The costs are real:
- The data is a third larger.
- The browser cannot cache the image separately from the page or style sheet that contains it.
- The page gets heavier before anything shows.
- Many e-mail clients do not display data URI images.
As a rule, keep data URIs for images of a few kilobytes at most. For anything bigger, a normal file is better; shrink it first with the image toolkit.
Using the Base64 tool
- Choose Encode or Decode.
- Type or paste, press "Open a file", or drop a file on the page (up to 20 MB). The result updates as you type.
- Under Alphabet, choose Standard or URL-safe. Under Padding with =, switch the padding on or off. Under Line length, pick one line, 64 characters (PEM) or 76 characters (MIME).
- Turn on As a data: URI to get a ready-to-paste value with the right prefix.
- Press Copy or Download. "Use as input" sends the result to the other tab, so you can check a round trip.
Decoding is forgiving. White space and line breaks are ignored, missing = is added, - and _ are accepted, and a whole data: URI is unwrapped. The tool says which of these it did. If the text is not Base64, it names the first wrong character and its position. When what you decode is not text, such as an image, a PDF or a ZIP, you get a download with the right type instead of garbage on screen, and an image also gets a preview. Everything runs in your browser, so a token or a document is not uploaded.
Common mistakes
- Treating Base64 as a way to hide a password or key. It is not. Anyone can reverse it.
- Encoding twice by accident. A string that decodes into another Base64-looking string has been encoded two times. Decode again.
- Mixing alphabets.
+and/in a token that should be URL-safe, or-and_in one that should not, make a strict decoder fail. - Cutting or adding characters. A stray quote, a space from copying, or a missing final character changes the result. A length with one character left over cannot be valid Base64.
- Using `btoa` on text with accents. Convert to UTF-8 bytes first.
- Assuming the output is smaller. It is always larger. Compression, not Base64, makes data smaller.
A short checklist
Remember that Base64 is encoding, not encryption. Turn text into UTF-8 bytes before encoding. Use the URL-safe alphabet for URLs and tokens, and check whether the padding is expected. Expect 33% more size, and keep data URIs for tiny files. When a decoded result looks wrong, check the character set before suspecting the Base64.