Coding
Base64 Explained: What It Is, and Why It Isn't Encryption
July 11, 2026 ยท 8 min read
Base64 is a way to represent binary data โ an image, a PDF, a cryptographic key, anything made of raw bytes โ using only 64 plain-text characters that virtually every system can handle. If you have ever seen a long run of letters, digits, and the occasional slash ending in one or two equals signs, that was almost certainly Base64.
The single most important thing to know up front: Base64 is encoding, not encryption. There is no key, no password, and no secret involved. Anyone who sees a Base64 string can decode it back to the original in one line of code or one paste into a free tool. It offers exactly zero confidentiality. Treating it as a way to hide a password or an API key is one of the most common security mistakes in software.
This guide covers what Base64 actually does, the step-by-step mechanics, the roughly 33% size penalty it adds, where it genuinely belongs (data URIs, email attachments, binary inside JSON), and how to encode and decode it in the shell, the browser, Node.js, and Python.
What Base64 actually is
Computers store everything as bytes, and a byte can hold any of 256 values. Plenty of channels, though, were designed to carry text, not arbitrary bytes. Old email servers only reliably passed 7-bit ASCII. JSON and XML are text formats. A URL, an HTML attribute, or a copy-pasted string can be mangled by anything that isn't a printable character. Base64 solves this by re-expressing binary data using a limited, universally safe alphabet.
That alphabet has 64 symbols โ the uppercase letters A to Z, the lowercase a to z, the digits 0 to 9, and two extras (a plus and a slash in the standard variant). Sixty-four is not arbitrary: it is exactly the number of distinct values you can represent with 6 bits (2 to the 6th power). Base64 chews through your data 6 bits at a time and maps each 6-bit chunk to one character from that table.
Because your data arrives in 8-bit bytes but leaves in 6-bit chunks, the numbers don't line up evenly. Base64 works on groups of 3 bytes (24 bits) at a time, splitting each group into 4 six-bit pieces. That 3-in, 4-out ratio is the source of both its structure and its size overhead, which we get to below.
How the encoding works, step by step
Take the word hello. As bytes, those five characters are 104, 101, 108, 108, 111. Grab the first three bytes and write them out in binary: 01101000 01100101 01101100. That's 24 bits. Now re-slice the exact same bits into four groups of six: 011010, 000110, 010101, 101100 โ which are the numbers 26, 6, 21, and 44.
Look each number up in the Base64 alphabet and you get a, G, V, s. So the first three bytes of hello become aGVs. The last two bytes (108, 111) are only 16 bits, one short of a full group. Base64 pads the missing bits with zeros to finish the last character, then appends an equals sign to mark that padding happened, giving bG8=. Put it together and hello encodes to aGVsbG8=. Decoding simply runs the whole process in reverse.
The equals sign at the end is padding, not data. Because Base64 always emits characters in blocks of four, a final block that started with only one leftover byte ends in ==, and one that started with two leftover bytes ends in a single =. That keeps the total length a clean multiple of four so a decoder knows exactly where the real bytes stop.
| Value range | Character(s) |
|---|---|
| 0 to 25 | A to Z |
| 26 to 51 | a to z |
| 52 to 61 | 0 to 9 |
| 62 | + (standard) or - (URL-safe) |
| 63 | / (standard) or _ (URL-safe) |
| padding | = (marks incomplete final block) |
Why it's not encryption (and never was)
Encoding, encryption, and hashing get lumped together constantly, but they solve different problems. Encoding (Base64, URL encoding) is a public, reversible transformation for safe transport โ no secret is involved and reversing it is expected. Encryption (AES, RSA) is reversible only if you hold the key, and its whole job is confidentiality. Hashing (SHA-256) is a one-way fingerprint used to verify integrity, not to hide or later recover the data.
Base64 sits firmly in the first bucket. There is no key to reverse it, because reversing it is the entire point. This is why an HTTP Basic Auth header is not secure on its own: the credentials Aladdin:open sesame become QWxhZGRpbjpvcGVuIHNlc2FtZQ==, which any observer can decode instantly. What actually protects those credentials is the HTTPS/TLS layer wrapped around the request, not the Base64 inside it.
JWTs make the same point. A JSON Web Token is three Base64URL-encoded parts joined by dots, and the header and payload are plain, readable JSON โ paste any JWT into a decoder and you can read every claim. The cryptographic signature stops someone from tampering with those claims; it does nothing to hide them. The rule that falls out of all this: never put anything you wouldn't want read into a Base64 string, a JWT payload, or a client-side token.
- Encoding = reversible, no key, for transport. Base64 and URL encoding live here.
- Encryption = reversible with a key, for confidentiality. This is what actually hides data.
- Hashing = one-way, no key, for integrity. SHA-256 verifies data hasn't changed.
- If you can read a Base64 string, so can everyone else โ it protects nothing.
The 33% tax: size and performance
Because Base64 turns every 3 bytes into 4 characters, the output is always about 4/3 the size of the input โ roughly a 33% increase. A 1 MB file becomes about 1.33 MB of text. On top of that, the email (MIME) form of Base64 inserts a line break every 76 characters, adding a little more, and padding rounds small inputs up further.
That overhead is the reason Base64 is a poor fit for large payloads. It is also the catch with data URIs. Embedding a big image directly in your HTML or CSS as base64 both inflates it by a third and prevents the browser from caching that asset separately โ the bytes get re-downloaded with the document every time instead of being served from cache as a standalone file. Inlining makes sense for tiny assets (a small icon, a critical above-the-fold sliver) where saving one request beats the size cost; it backfires on anything substantial.
When Base64 is genuinely too fat, the alternatives are either a more compact text encoding or, better, sending real binary over a channel that supports it. Here's how the common text encodings compare:
| Encoding | Alphabet | Size vs raw | Notes |
|---|---|---|---|
| Hex (Base16) | 16 | +100% | Two characters per byte; case-insensitive and easy to read |
| Base32 | 32 | ~+60% | Case-insensitive, avoids look-alike characters; good for typing aloud |
| Base64 | 64 | +33% | Most compact of the common text encodings |
| Base64 (MIME) | 64 | ~+37% | Adds a line break every 76 characters for email |
When to actually use Base64
Base64 earns its keep whenever binary data has to travel through a text-only pipe. The classic cases are stable and worth knowing by name, and each comes with a caveat about when to reach for something else instead.
Reach for Base64 when the transport is genuinely text and the payload is modest. Reach for raw binary transfer, a URL to the asset, or streaming when the payload is large or performance-sensitive.
- Email attachments: MIME wraps every non-text attachment in Base64 so it survives mail servers. This is automatic โ you rarely encode it by hand.
- Data URIs: data:image/png;base64,... embeds a small image or font inline. Great for tiny, critical assets; wasteful for large ones because of the size and caching costs above.
- Binary inside JSON or XML: those formats can't hold raw bytes, so a file, a thumbnail, or a key gets Base64-encoded into a string field. Fine for small blobs; prefer a file upload or URL reference for anything big.
- Tokens and identifiers: JWTs, signed cookies, and API tokens use Base64URL to pack bytes into a URL- and header-safe string. Remember the payload is readable, not secret.
- Copy-paste transport: pasting a certificate, a small binary, or config into a chat or terminal without it getting corrupted.
How to encode and decode Base64
Every mainstream environment has Base64 built in. On the command line, macOS and Linux ship a base64 utility; the -n on echo matters because it prevents a trailing newline from sneaking into your input. In the browser, btoa and atob exist but have a Unicode trap (covered in the FAQ) โ for anything beyond plain ASCII, encode the bytes yourself. Node.js and Python handle UTF-8 correctly out of the box.
One decoding gotcha across all of them: most decoders tolerate surrounding whitespace and newlines but will reject an unexpected character or, sometimes, missing padding. If a decode fails, check first for a standard-versus-URL-safe mismatch (+ and / versus - and _) or a stripped equals sign.
- Standard Base64 uses + and /; URL-safe Base64 uses - and _ and often drops padding.
- base64 -d decodes; add -w 0 on Linux to stop it wrapping output at 76 characters.
- For a quick one-off, an in-browser encoder/decoder avoids any command-line quoting headaches.
| Environment | Encode | Decode |
|---|---|---|
| Linux / macOS shell | echo -n hi | base64 | echo aGk= | base64 -d |
| PowerShell | [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('hi')) | [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('aGk=')) |
| JavaScript (browser) | btoa('hi') (ASCII only) | atob('aGk=') |
| Node.js | Buffer.from('hi').toString('base64') | Buffer.from('aGk=','base64').toString() |
| Python | base64.b64encode(b'hi') | base64.b64decode('aGk=') |
Base64URL and the gotchas that bite people
The variant you'll meet most often after standard Base64 is Base64URL. It encodes identically but swaps the two problem characters: + becomes -, / becomes _, and the = padding is frequently omitted. This matters because a raw + or / breaks inside URLs (where + can mean a space) and filenames. If a token decodes fine in one place and errors in another, a standard-versus-URL-safe mismatch is the usual culprit.
A second trap is Unicode in the browser. The built-in btoa expects each character to fit in a single byte, so handing it an emoji or most non-English text throws a range error. The correct approach is to turn the string into UTF-8 bytes first with a TextEncoder, then Base64-encode those bytes โ or lean on Node's Buffer, Python, or a tool that does UTF-8 for you.
Finally, don't confuse Base64 with a checksum or a validator. It changes how bytes are represented; it does nothing to verify that they arrived intact or unaltered. If you need to confirm a download or a payload wasn't corrupted or tampered with, that's a job for a hash like SHA-256, not for Base64.
Frequently Asked Questions
No. Base64 has no key and no secret, so decoding it is a one-line operation in any language or a single paste into a free tool. If you can read the string, so can anyone else. Base64 inside an HTTP Basic Auth header or a JWT protects nothing on its own โ the actual security comes from the HTTPS/TLS layer around it. To keep a secret confidential you need encryption; to verify it you need a hash. Never rely on Base64 for either.
It packs 3 bytes (24 bits) of input into 4 output characters, and each of those characters carries only 6 bits of real information. Four thirds is 1.333, so the output runs roughly 33% larger than the input. Padding on the final block, and the line breaks that email (MIME) adds every 76 characters, push it slightly higher. Expect a 1 MB file to become about 1.33 MB of Base64 text.
They encode data identically; only two characters differ. Standard Base64 uses + and /, which have special meaning in URLs and filenames, while URL-safe Base64 swaps them for - and _. URL-safe strings also frequently drop the = padding. If you paste a token into a decoder and it errors out, a +// versus -/_ mismatch or missing padding is the most likely reason.
Yes. Base64 operates on raw bytes, so it doesn't care whether those bytes form a PNG, a ZIP, a PDF, or UTF-8 text โ it treats them all the same. That universality is the whole point: it's a reliable way to carry arbitrary binary through a channel that only handles text safely, such as JSON, XML, an email body, or a copy-pasted string.
That's padding. Base64 works in blocks of 3 input bytes to 4 output characters. When the final block has only 1 leftover byte you get ==, and 2 leftover bytes give a single =. The padding keeps the total length a multiple of four so a decoder knows exactly where the data ends. URL-safe Base64 often omits it, and many decoders will accept unpadded input.
Only for Latin-1, single-byte characters. Passing an emoji or most non-English text to btoa() throws a character-out-of-range error, because it expects every character to fit in one byte. The modern fix is to convert your string to bytes first with new TextEncoder().encode(str) and Base64-encode those bytes โ or use Node's Buffer, Python, or a tool that handles UTF-8 for you.
Related Tools
Paste a string and convert it to or from Base64 instantly in your browser โ nothing is uploaded.
encode or decode a URLPercent-encode query strings and special characters, or decode them back to readable text.
hash data with SHA-256 for real integrityGenerate one-way SHA-256 and other hashes when you need verification, not reversible encoding.