Last year I took over an operations back office whose list page took over nine seconds to open. Packet capture showed the API itself taking 300 milliseconds, yet returning a 22MB body: every record embedded the full product image in the JSON, with a field like "pic": "data:image/jpeg;base64,/9j/4AAQSkZJRg...". One original image was 850KB, 1.13MB once encoded, and twenty records per page blew up to that size. The person who wrote the code had a perfectly reasonable argument: it saves one image request. To understand why that argument only holds for very small images, you need to be clear about what Base64 is.
1. It solves transport, never protection
Base64 does one narrow job: it turns an arbitrary byte stream into text written with only 64 printable characters, so it can pass safely through channels that only accept text, which includes JSON, XML, mail headers and URL query strings. Early mail protocols could only carry 7-bit ASCII, so binary attachments had to be reworded, and a JSON field is the same story, since raw bytes break the parser.
But rewording is not encryption. There is no key and no passphrase, and anyone can turn the result back into the original. I have seen a real case where a project Base64-encoded users' phone numbers into its logs and was finished by a single question in a security review: how is this different from leaving them alone. JWT works the same way: its header and payload are only Base64Url-encoded, so pasting the string into a console reveals phone numbers, roles and expiry. Real security comes from the third section, the signature, not from that encoding layer.
2. Why it grows, and where the trailing = comes from
The alphabet is A-Z, a-z, 0-9, + and /. The rule is to cut 3 bytes, 24 bits, into 4 chunks of 6 bits each, and each chunk indexes a character from 0 to 63. A partial group at the end is padded with =, which is why one or two equals signs often appear at the end. The cost is fixed by arithmetic: 3 bytes become 4 characters, so the size grows to 4/3 of the original.
| Input | UTF-8 bytes | Encoded | Note |
|---|---|---|---|
| Man | 3 | TWFu | Exactly one group, no padding |
| hi | 2 | aGk= | Padded with one = |
| hello | 5 | aGVsbG8= | 5 is not a multiple of 3 |
| Zhang San | 6 | 5byg5LiJ | A Chinese character is about 3 bytes, so exactly two groups |
That table also explains two everyday questions: why Chinese text always produces a Base64 length that is a multiple of 4, because a Chinese character is usually 3 bytes in UTF-8, and why a trailing = is not an error but padding.
3. When inlining an image actually pays
A data URI is written as data:[MIME type];base64,[content] and can go straight into <img src> or serve as a CSS background. What it saves is one TCP connection and request round trip; the cost is that the content travels with the HTML and cannot be cached separately by the browser, so changing the home page HTML forces the whole inlined image to be downloaded again.
My rule of thumb sits at 2KB: small icons, arrows and placeholder shapes under that are worth inlining; anything larger becomes a separate file with a fingerprint and a long cache lifetime. The product image in that outage was 850KB, 1.13MB encoded, and gzip could not rescue it, because the redundancy in the original binary has already been broken up by 6-bit grouping and the compression ratio is clearly worse than compressing the original file. For similar cases I keep large images on the CDN and inline only 16x16 icons, where a 30 percent size increase is entirely acceptable.
Also unsuitable for inlining: any image a user can upload, any image that needs to be indexed or reused, and any image that needs its own Cache-Control.
4. What I actually do now
- When debugging an API and I need to recover a suspicious string, I do it in the browser, which saves effort and removes one more worry: text, images and files all run through Base64 Encoder and Decoder, locally and without upload; decoding Chinese needs no manual encoding handling, since the page understands UTF-8.
- For a small icon to embed in CSS, I convert the image on the same page to a result with the
data:image/...;base64,prefix and paste it into the stylesheet, then estimate the original file size times 4/3 to decide whether inlining is really worth it. - For turning arbitrary files such as audio or PDFs into a string, File to Base64 lists both the original and the encoded size; very large files fail because of browser memory limits, and at that point you want chunked upload, not brute force.
- For a value that has to go into a URL parameter, run it through URL Encoder and Decoder first; do not expect a Base64 result to sit unchanged in a query string.
5. Traps I only learned by hitting them
- The plus sign gets eaten:
+in the Base64 alphabet means a space under the old query string convention. When a valid encoded string decodes to something completely different, this is usually why. Either urlencode first, or switch to Base64Url, which maps+and/to-and_and omits the=padding, and which is what JWT uses. atobmangles Chinese:atob()returns a string assembled byte by byte, so Chinese falls apart into things likeö¼¡. Either usedecodeURIComponent(escape(atob(s)))or useTextDecoder; in Node the correct approach isBuffer.from(s,'base64').toString('utf8').- Line breaks break decoding: long strings copied from mail, documents or logs often carry
\r\nor spaces, and Python'sb64decodethrowsbinascii.Error: Invalid base64-encoded string. Strip the whitespace first. - Length out of alignment: a Base64 result is always a multiple of 4. If you get 43 characters, the start or the end was truncated, which is very common in the clipped display of a packet capture tool.
- Wrong MIME type: writing a PNG with the prefix
data:image/jpeg;base64,does not necessarily raise an error in the browser, but it renders as a broken image; when assembling by hand, let the tool generate the whole string.
6. Common questions
Q: Can Base64 be used as encryption?
No. It is a reversible encoding with no key, and anyone holding the ciphertext can restore it. If you need secrecy, use a real encryption algorithm; if you need to store passwords, use a salted hash.
Q: Why is my encoded output a third larger?
Because every 3 bytes must be expressed with 4 characters. That is the fixed overhead of carrying binary in printable characters, and it has nothing to do with whether the content is an image.
Q: Does inlining small images really speed things up?
At two or three KB, saving a request round trip is worth more than the size increase; beyond that it is a net loss, and a separate file with caching is faster.
Q: Should the one or two trailing = be removed?
In standard Base64 they are padding characters, and leaving them in is the safest option when decoding. Some languages allow strict validation, where removing them raises a length error instead.
Q: Can Base64 be used to transfer large files?
Not advisable. The size grows by a third, the whole payload must be read into memory, and streaming is ruled out; anything in the megabytes belongs in object storage or a chunked upload.
Looking back at that nine-second list page, the fix was one line: replace the image field with a URL and keep inlining only for icons. The test is simpler too: ask whether this content needs to be carried across or hidden, and ask whether it is under 2KB, and once both answers are in, whether to use Base64 is rarely a hard call.