What Is Base64 Encoding? The 33% Overhead Explained
Base64 writes any byte as one of 64 safe ASCII characters, which makes binary survivable in text-only channels and costs about 33% more space. It is an encoding, not encryption, and nobody needs a key to undo it. What the alphabet and the padding rules actually specify is below; if you just need a string turned into Base64, the Base64 Encoder / Decoder tool linked from this guide runs in your browser.
Free and local, with full UTF-8 support, so emoji and non-Latin scripts round-trip. It also reads the URL-safe variant and ignores the line wrapping a PEM file leaves behind.
encode or decode Base64 text in your browser →Why binary needs a text encoding
Many transports only reliably carry 7-bit text. SMTP, for instance, was specified to move 7-bit US-ASCII, and a message containing a null byte or a byte with the high bit set is at best mangled and at worst rejected. Base64 maps every possible byte value onto a subset of 64 printable characters, so the output is safe to paste into a mail body, a JSON string, a cookie, or a URL query. Because 64 is 2^6, six bits of input map to exactly one output character with no arithmetic loss, which is what makes the encoding reversible. The historical use is MIME, where email attachments are carried as multipart bodies with each part Base64-encoded. Data URIs use the same idea to inline a small image in a stylesheet. The cost is that the output is a third larger than the input, and that cost is real, because you pay it in bandwidth and in memory on every transfer. The image tool here uses FileReader to read a file as a data URL in the browser, with no upload, and refuses files above 8 MB because the encoded form would be 10.7 MB and would stall the page. Every one of those choices — which alphabet, how much padding, whether the result is safe to drop into a URL — is a rule rather than a judgement call, and the encoder that applies them for you is linked from this page.
How encoding works step by step
Take the three ASCII bytes for Man, which are 0x4D, 0x61 and 0x6E, or 01001101 01100001 01101110 in binary. That is 24 bits. Split them into four groups of six: 010011, 010110, 000101, 101110, which are 19, 22, 5 and 46 in decimal. Index the standard alphabet A to Z for 0 to 25, a to z for 26 to 51, 0 to 9 for 52 to 61, and + then / for 62 and 63. That gives T, W, F, u, so Man encodes to TWFu. Check it in a browser with btoa('Man'). The partial cases are where hand-written implementations go wrong. One byte, M, is 01001101, padded with four zero bits into 010011 and 010100, which is 19 and 20, or T and Q, plus two padding characters: TQ==. Two bytes, Ma, is padded into 010011, 010110, 000100, giving T, W, E, plus one pad: TWE=. A partial final group is always filled with zero bits on the right, which is why a canonical encoder and a sloppy one can produce different text for the same bytes. Emoji take the multi-byte path: the UTF-8 bytes for U+1F600 are F0 9F 98 80, which encode to 8J+YgA==, twelve characters from one.
Padding rules and what a decoder must reject
The = character is padding, and it exists so the output length is always a multiple of 4, which lets a decoder know how many groups to expect. A valid encoded string ends with no padding, with one =, or with two =. Three is impossible, because that would mean fewer than one input byte. The output length is exactly 4 times the ceiling of the input length divided by 3, so 1 byte becomes 4 characters, 2 becomes 4, 3 becomes 4, 4 becomes 8, 5 becomes 8, and 6 becomes 8. That is why the overhead is not a constant 33%: a 1-byte input costs 300% overhead and a 6-byte input costs 33.3%, converging on 33.6% for anything large. A 10 MB image encodes to 13.33 MB. A strict decoder should reject a length where the remainder modulo 4 is 1, since that cannot arise from any byte sequence, and should reject a = appearing anywhere except the tail. A tolerant decoder, usually what you want, strips whitespace first, because PEM files wrap at 64 characters per line.
The Base64URL variant and JWT usage
Two characters in the standard alphabet are awkward in a URL. The + is commonly decoded as a space in query strings, and / is the path separator, so an unescaped Base64 token in a query or a path gets corrupted in transit. RFC 4648 section 5 defines the URL and filename safe alphabet, which swaps + for a hyphen and / for an underscore, and permits the padding to be dropped. In JavaScript: btoa(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''). To decode, reverse it and restore the padding, since the stripped length tells you how much is missing: const pad = len % 4 ? 4 - (len % 4) : 0. A JWT is three Base64URL segments, header, payload and signature. The standard HS256 header is eyJhbGciOiJIUzI1NiJ9, which decodes to {"alg":"HS256"}. The important security point is that a JWT payload is encoded, not encrypted. Anyone holding the token can read its contents. It is signed, and the signature lets the server verify nobody altered it, but signing is not secrecy, so never put a password in the payload.
When to use Base64 and when not to
Use it when the payload is genuinely binary and the channel is genuinely text: email attachments, an embedded font or icon, a small image in a data URI, a JSON field holding a certificate. The overhead is acceptable when the payload is small. Avoid it elsewhere. For images and video on the web, a normal fetch with a cacheable URL is better: a data URI is inlined into the document, so it is re-downloaded with every page load and cannot be cached separately. A 33% larger image is a real cost on mobile data. For database storage, store the bytes; a Base64 column is a third larger and cannot be indexed usefully. For a URL parameter, Base64 is verbose, since it costs 4 characters for every 3. For a password, never. Base64 is not protection, it is transport, and treating it as a secret has ended more projects than any cipher weakness. The nuance worth keeping: Base64 is not encryption, but it is not nothing either, since it keeps binary out of contexts that mangle it and makes a secret's presence in a log obvious to a human reading it.
Code examples you can copy
The Unicode-safe JavaScript encoder, because btoa on its own throws on any character above U+00FF: function b64EncodeUnicode(str) { const bytes = new TextEncoder().encode(str); let bin = ''; bytes.forEach(b => bin += String.fromCharCode(b)); return btoa(bin); }. The matching decoder: function b64DecodeUnicode(b64) { const bin = atob(b64); const bytes = Uint8Array.from(bin, c => c.charCodeAt(0)); return new TextDecoder().decode(bytes); }. In Node, Buffer.from(str, 'utf8').toString('base64') and the reverse with Buffer.from(b64, 'base64').toString('utf8'). In Python, base64.b64encode(b'hello').decode() and base64.b64decode(b64), or base64.urlsafe_b64encode for the URL-safe variant. For a file, read the bytes and encode those rather than encoding a string, since a text read corrupts anything that is not valid UTF-8. To check the overhead without a library, compare Buffer.from(data).toString('base64').length against data.length. Always verify with a round trip, decode(encode(x)) equals x, and test with a non-ASCII string, because a round trip that passes for ASCII can still fail for an emoji.