Base64: why your files grow 33% and when that is fine

Updated 2026-08-09

Base64 turns every 3 bytes of binary data into 4 characters of text. That single ratio explains almost everything people find confusing about it: the 33% size growth, the trailing = signs, why a 25 MB attachment becomes a 33 MB email, and why stuffing large files into JSON payloads is usually a mistake. The encoding exists because a lot of infrastructure — JSON, HTML, email bodies, HTTP headers, environment variables — can only carry text safely, and Base64 is the standard way (RFC 4648) to smuggle arbitrary bytes through a text-only channel.

This guide works through the exact byte math, the padding rules, the base64url variant, the practical PDF-to-Base64 round trip, and — just as important — the cases where you should refuse to Base64 anything. If you want to follow along interactively, the site's Base64 encoder, File to Base64, PDF to Base64, and Base64 to PDF tools all run client-side, so you can experiment without uploading anything.

The 4/3 rule, byte for byte

Base64 uses an alphabet of 64 characters (A-Z, a-z, 0-9, + and /). 64 = 2^6, so each output character carries exactly 6 bits. Three input bytes are 24 bits, which map cleanly onto four 6-bit characters. Output length is therefore always ceil(n/3) * 4 characters for n input bytes — a fixed 4/3 expansion, or +33.33%, with a tiny extra bump from padding on the last block.

Verify it yourself
$ printf 'Hi' | base64
SGk=

$ python3 -c "import base64; print(base64.b64encode(b'Man'))"
b'TWFu'

# 3 bytes in, 4 chars out. For a 1 MiB file:
$ python3 -c "import base64; print(len(base64.b64encode(bytes(1048576))))"
1398104
Input size (bytes)Base64 length (chars)Growth
1,024 (1 KiB)1,368+33.59%
1,048,576 (1 MiB)1,398,104+33.33%
10,485,760 (10 MiB)13,981,016+33.33%
26,214,400 (25 MiB)34,952,536+33.33%

Note the 1 KiB row: small inputs pay proportionally slightly more because the final partial block is always padded to a full 4 characters. At megabyte scale the overhead converges to exactly one third.

What the = signs actually mean

Base64 output length is always a multiple of 4. When the input length is not a multiple of 3, the encoder pads: one leftover byte produces two = signs, two leftover bytes produce one. Encoding the single byte 'A' gives QQ== and the two bytes 'Hi' give SGk= — verified above. The padding carries no data; it just tells the decoder how many bytes the final block really held. Some decoders (including JavaScript's atob in most browsers) tolerate missing padding, but many strict parsers do not, so never trim = signs from a string you plan to hand to another system.

Data URIs, JSON payloads, and the email tax

A data URI is just a MIME type plus Base64: data:application/pdf;base64,JVBERi0x... — and that JVBERi0x prefix is not random. It is the Base64 encoding of the ASCII bytes %PDF-1, the magic number every PDF starts with. If someone hands you a 'Base64 PDF' that does not start with JVBER, it is not a PDF. The same trick works for other formats: PNG data URIs start with iVBOR, JPEGs with /9j/.

Base64 is popular inside JSON APIs for a quiet reason: all 65 possible output characters (the alphabet plus =) are valid inside a JSON string with zero escaping. Raw binary would need \uXXXX escapes that can triple the size; Base64's predictable +33% is the cheaper deal. It is still a real cost, though — a 25 MB file becomes a 34,952,536-byte string field, which your JSON parser must hold in memory as one contiguous blob.

Email is worse. MIME requires Base64 bodies to be wrapped at 76 characters per line with CRLF line endings. Wrapping a 1 MiB attachment that way produces 1,434,898 bytes — a 36.84% overhead, not 33%. That is why a mailbox with a 25 MB attachment limit effectively rejects files well under 25 MB.

base64 vs base64url: two characters of difference

Standard Base64 uses + and /, both of which are meaningful inside URLs (+ is a space in query strings, / is a path separator). The base64url variant from RFC 4648 section 5 swaps them and usually drops padding:

Variant62nd char63rd charPaddingTypical use
base64+/= requiredMIME, data URIs, JSON fields
base64url-_usually omittedJWTs, URL parameters, filenames
Same bytes, two encodings
$ python3 -c "import base64; b=bytes([251,239,190]); \
  print(base64.b64encode(b), base64.urlsafe_b64encode(b))"
b'++++' b'----'

This is why pasting a JWT segment into a standard Base64 decoder sometimes fails: every - and _ must be mapped back to + and / (and padding restored) first. It is also why 'it decodes on my machine' bugs happen — some libraries silently accept both alphabets, others reject the wrong one.

The PDF round trip, and a UTF-8 trap in the browser

The common workflow — embed a PDF in a JSON payload, store it, later reconstruct the file — is safe as long as you treat the data as bytes end to end. In a terminal: openssl base64 -A -in file.pdf produces the string; base64 -d (or openssl base64 -d -A) reverses it. In the browser, the PDF to Base64 tool reads the file locally and gives you both the raw string and the data URI; Base64 to PDF accepts either form (with or without the data:application/pdf;base64, prefix), rebuilds the bytes with atob into a Uint8Array, and offers a download plus an inline preview. Nothing is uploaded in either direction.

The trap to know about is text, not files. JavaScript's btoa operates on Latin-1 code points, not UTF-8. btoa('é') happily returns 6Q== — the Latin-1 byte 0xE9 — while every UTF-8 consumer expects w6k= (the two bytes 0xC3 0xA9). Feed btoa anything outside Latin-1, like an emoji, and it throws InvalidCharacterError. The correct pattern, and the one the site's Base64 encoder uses, is TextEncoder first, then encode the resulting bytes; decoding runs TextDecoder with fatal: true so corrupted input fails loudly instead of producing mojibake.

When not to Base64

  • File uploads: multipart/form-data exists precisely so browsers can send raw bytes. Base64-ing an upload adds 33% wire cost and forces the server to decode before it can stream to disk.
  • Large files in JSON: the whole string must sit in memory at once, and in JavaScript that string is UTF-16 internally, roughly doubling RAM again. Use a presigned upload URL and send a reference instead.
  • Anything you plan to compress afterward: gzip on Base64 output compresses far worse than gzip on the original bytes, because the encoding destroys byte-level patterns. Compress first, encode second — if you must encode at all.
  • As 'encryption': Base64 is a reversible public encoding. Anyone can decode it instantly; it hides nothing.

Base64 earns its 33% when the channel genuinely cannot carry binary: a config value, a small icon inlined in CSS, a signing key in an environment variable, a PDF inside a JSON API you do not control. Everywhere else, send the bytes.

Questions people ask

Does Base64 encrypt or protect data?

No. It is a public, reversible encoding with no key. Anyone can decode it in one line of code. If you need confidentiality, encrypt first (for example with AES), then Base64 the ciphertext for transport.

Why does my Base64 string end in one or two = signs?

Padding. Base64 output is always a multiple of 4 characters; == means the final block encoded one byte, = means it encoded two. Keep the padding — strict decoders reject strings without it.

Why is my Base64 exactly the file size plus one third, but email says the attachment is even bigger?

MIME wraps Base64 at 76 characters per line with CRLF endings, pushing the overhead from 33.33% to about 36.84%. That difference is measurable: a 1 MiB file becomes 1,398,104 characters raw but 1,434,898 bytes once wrapped.

Can I decode a JWT with a normal Base64 decoder?

Only after converting: JWTs use base64url, so replace - with +, _ with /, and re-add padding to a multiple of 4. Or use a decoder that handles base64url natively, like the site's JWT decoder.

Try it yourself