Base64 isn't encryption. It's not compression. It's a way to represent binary data as text — and understanding it will save you hours of debugging.
I've lost count of how many times I've watched a developer stare at a Base64 string and say "I don't know what this is." It's in their JWT tokens. It's in their API responses. It's in their email headers. It's literally the encoding that makes half the internet work, and most people treat it like some mysterious black box.
Let me fix that.
By the end of this post, you'll understand exactly what Base64 is, why someone invented it in the first place, how the algorithm works under the hood, where you'll encounter it in the real world, and how to encode and decode it in every language you're likely to use. You'll also understand why using it as "encryption" is one of the most dangerous misconceptions in software development.
Base64 is an encoding scheme. That's it. It takes binary data — any sequence of bytes — and converts it into a string of ASCII characters. Specifically, it uses 64 characters: A-Z, a-z, 0-9, +, and /. Plus = for padding, which we'll get to.
Here's what Base64 is not:
Base64 is a transport encoding. Its entire purpose is to take data that might not be safe to transmit through text-based systems and make it safe.
The year is 1992. Email is exploding. But email systems have a problem: they were designed to transmit ASCII text. Specifically, 7-bit ASCII. That means every byte in an email must have a value between 0 and 127. Many systems would corrupt or drop anything outside that range.
Now someone wants to send a photo via email. A JPEG file. JPEGs contain bytes with values from 0 to 255 — the full 8-bit range. If you just shove those raw bytes into an email, mail servers along the route will mangle them. Bytes above 127 get stripped or replaced. Null bytes get interpreted as string terminators. Line-ending conversions turn \n into \r\n or vice versa, corrupting binary data.
The solution was MIME (Multipurpose Internet Mail Extensions), and specifically the Content-Transfer-Encoding: base64 header. Take those binary bytes, convert them into a safe subset of ASCII characters, and now any email server on the planet can handle them without corruption.
That's the origin story. But the use cases have expanded massively since 1992.
I'm not going to hand-wave this. Understanding how Base64 works will make everything else click. And it's simpler than you think.
Take your input data and express it as a sequence of bits. For example, the text "Hi":
H = 72 = 01001000
i = 105 = 01101001
So "Hi" in binary is: 01001000 01101001
Base64 uses 64 characters. 64 = 2^6. So we need 6 bits to represent each Base64 character.
Take those bits and split them into groups of 6:
010010 | 000110 | 1001xx
Wait — we only had 16 bits (2 bytes), but 6-bit groups need multiples of 6. We have 16 bits, which gives us 2 full groups of 6 (12 bits) with 4 bits left over. So we pad the remaining bits with zeros:
010010 | 000110 | 100100
Each 6-bit value (0–63) maps to a character:
| Value | Char | Value | Char | Value | Char | Value | Char |
|---|---|---|---|---|---|---|---|
| 0 | A | 16 | Q | 32 | g | 48 | w |
| 1 | B | 17 | R | 33 | h | 49 | x |
| 2 | C | 18 | S | 34 | i | 50 | y |
| 3 | D | 19 | T | 35 | j | 51 | z |
| 4 | E | 20 | U | 36 | k | 52 | 0 |
| 5 | F | 21 | V | 37 | l | 53 | 1 |
| 6 | G | 22 | W | 38 | m | 54 | 2 |
| 7 | H | 23 | X | 39 | n | 55 | 3 |
| 8 | I | 24 | Y | 40 | o | 56 | 4 |
| 9 | J | 25 | Z | 41 | p | 57 | 5 |
| 10 | K | 26 | a | 42 | q | 58 | 6 |
| 11 | L | 27 | b | 43 | r | 59 | 7 |
| 12 | M | 28 | c | 44 | s | 60 | 8 |
| 13 | N | 29 | d | 45 | t | 61 | 9 |
| 14 | O | 30 | e | 46 | u | 62 | + |
| 15 | P | 31 | f | 47 | v | 63 | / |
Our 6-bit groups 010010, 000110, 100100 map to values 18, 6, 36, giving us: SGk
Base64 output must always be a multiple of 4 characters. We have 3 characters, so we add one = pad:
SGk=
And that's "Hi" in Base64. Go verify it — open your browser console and type btoa("Hi"). You'll get SGk=.
This confuses people more than anything, so let me make it crystal clear:
= signs. 1 leftover byte = 2 Base64 chars + ==.= sign. 2 leftover bytes = 3 Base64 chars + =.That's it. The = signs are just telling the decoder "hey, those last few bits were padding, not real data."
Every 3 bytes of binary data become 4 characters of Base64 text. That's a 33.33% increase. Always.
This means:
This isn't a bug — it's the fundamental trade-off. You gain universal text-safe transport. You pay with size.
For most use cases, this is fine. For large files, it's worth considering whether Base64 is really the right approach or whether you should be using binary transport (multipart form data, binary WebSocket frames, etc.).
Here's where you'll actually encounter Base64 in your day-to-day work.
Instead of referencing an external image file, you can embed it directly:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA
AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBA
AO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />Or in CSS:
.icon-star {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJsMy4wOSA2LjI2TDIyIDkuMjdsLTUgNC44NyAxLjE4IDYuODhMMTIgMTcuNzdsLTYuMTggMy4yNUw3IDEzLjE0IDIgOC4yN2w2LjkxLTEuMDFMMTIgMnoiLz48L3N2Zz4=');
}When this makes sense: Small icons, tiny images (under 2-3 KB), SVGs. It eliminates an HTTP request, which can matter for performance on high-latency connections.
When this is a terrible idea: Anything over a few KB. You lose browser caching, you bloat your HTML/CSS, and that 33% size penalty means you're transferring more data total, not less.
Every JWT you've ever seen is Base64-encoded. Well, technically Base64URL-encoded (more on that in a minute). A JWT has three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Each of those three sections is just a Base64URL-encoded JSON object:
// Decode the header (first section)
atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')
// Result: {"alg":"HS256","typ":"JWT"}
// Decode the payload (second section)
atob('eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ')
// Result: {"sub":"1234567890","name":"John Doe","iat":1516239022}The third section is the signature — which IS cryptographic, but the header and payload are just Base64. Anyone can read them. This is by design. JWTs are not encrypted; they're signed. The signature proves the payload hasn't been tampered with, but it doesn't hide the payload.
If you've been storing sensitive data in JWT payloads thinking "it's encoded, nobody can read it" — now you know better.
The Authorization: Basic header uses Base64:
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
Decode that:
atob('dXNlcm5hbWU6cGFzc3dvcmQ=')
// "username:password"Yeah. Your "Basic Auth" credentials are literally just username:password in Base64. This is why Basic Auth should never be used without HTTPS. Over plain HTTP, anyone sniffing the traffic can decode your credentials in a fraction of a second.
Many APIs use Base64 to embed binary data in JSON payloads. Since JSON is a text format and can't natively represent binary data, Base64 is the standard escape hatch:
{
"filename": "report.pdf",
"content": "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YW...",
"content_encoding": "base64"
}GitHub's API does this for file contents. Stripe does it for PDF invoices. AWS does it for Lambda payloads. It's everywhere.
Every email attachment you've ever sent was Base64-encoded. Open the raw source of any email with an attachment and you'll see:
Content-Type: application/pdf; name="invoice.pdf"
Content-Transfer-Encoding: base64
JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZwov
UGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5
cGUgL1BhZ2VzCi9LaWRzIFszIDAgUl0KL0NvdW50IDEgPj4K
...
That wall of seemingly random characters? That's your PDF, Base64-encoded, safe for transit through any email server on Earth.
Standard Base64 uses + and / as two of its 64 characters. That's fine for email, but it's a problem for URLs. Both + and / have special meanings in URLs (+ can mean space, / separates path segments).
Base64URL replaces them:
+ becomes -/ becomes _= is typically omittedHere's a comparison:
| Feature | Base64 | Base64URL |
|---|---|---|
| Character 62 | + | - |
| Character 63 | / | _ |
| Padding | Required (=) | Usually omitted |
| URL safe | No | Yes |
| Used in | Email, data URIs, general encoding | JWTs, URL parameters, filenames |
This distinction causes so many bugs. You decode a JWT with a standard Base64 decoder and it chokes. You put a Base64 string in a URL and the server interprets it wrong. You strip padding and the decoder throws an error.
When you're debugging Base64 issues, the first question should always be: "Is this standard Base64 or Base64URL?"
Here's your reference sheet. Bookmark this section.
// Encode
const encoded = btoa('Hello, World!');
// "SGVsbG8sIFdvcmxkIQ=="
// Decode
const decoded = atob('SGVsbG8sIFdvcmxkIQ==');
// "Hello, World!"
// Handle Unicode (btoa only works with Latin-1)
const unicodeEncode = btoa(unescape(encodeURIComponent('Héllo 🌍')));
const unicodeDecode = decodeURIComponent(escape(atob(unicodeEncode)));
// Modern approach for Unicode (TextEncoder/TextDecoder)
function toBase64(str) {
const bytes = new TextEncoder().encode(str);
const binString = Array.from(bytes, b => String.fromCodePoint(b)).join('');
return btoa(binString);
}
function fromBase64(b64) {
const binString = atob(b64);
const bytes = Uint8Array.from(binString, c => c.codePointAt(0));
return new TextDecoder().decode(bytes);
}// Encode
const encoded = Buffer.from('Hello, World!').toString('base64');
// "SGVsbG8sIFdvcmxkIQ=="
// Decode
const decoded = Buffer.from('SGVsbG8sIFdvcmxkIQ==', 'base64').toString('utf-8');
// "Hello, World!"
// Base64URL variant
const urlEncoded = Buffer.from('Hello, World!').toString('base64url');
// "SGVsbG8sIFdvcmxkIQ"
// Encode a file
const fs = require('fs');
const fileBase64 = fs.readFileSync('photo.jpg').toString('base64');
// Decode to file
fs.writeFileSync('output.jpg', Buffer.from(fileBase64, 'base64'));import base64
# Encode
encoded = base64.b64encode(b'Hello, World!').decode('utf-8')
# "SGVsbG8sIFdvcmxkIQ=="
# Decode
decoded = base64.b64decode('SGVsbG8sIFdvcmxkIQ==').decode('utf-8')
# "Hello, World!"
# Base64URL
url_encoded = base64.urlsafe_b64encode(b'Hello, World!').decode('utf-8')
# "SGVsbG8sIFdvcmxkIQ=="
# Encode a file
with open('photo.jpg', 'rb') as f:
file_b64 = base64.b64encode(f.read()).decode('utf-8')
# Decode to file
with open('output.jpg', 'wb') as f:
f.write(base64.b64decode(file_b64))package main
import (
"encoding/base64"
"fmt"
)
func main() {
// Standard encoding
encoded := base64.StdEncoding.EncodeToString([]byte("Hello, World!"))
fmt.Println(encoded) // SGVsbG8sIFdvcmxkIQ==
// Decode
decoded, _ := base64.StdEncoding.DecodeString(encoded)
fmt.Println(string(decoded)) // Hello, World!
// URL-safe encoding
urlEncoded := base64.URLEncoding.EncodeToString([]byte("Hello, World!"))
fmt.Println(urlEncoded) // SGVsbG8sIFdvcmxkIQ==
// No-padding variant
noPad := base64.RawURLEncoding.EncodeToString([]byte("Hello, World!"))
fmt.Println(noPad) // SGVsbG8sIFdvcmxkIQ
}import java.util.Base64;
// Encode
String encoded = Base64.getEncoder().encodeToString("Hello, World!".getBytes());
// "SGVsbG8sIFdvcmxkIQ=="
// Decode
byte[] decodedBytes = Base64.getDecoder().decode(encoded);
String decoded = new String(decodedBytes);
// "Hello, World!"
// URL-safe
String urlEncoded = Base64.getUrlEncoder().encodeToString("Hello, World!".getBytes());
// No-padding
String noPadding = Base64.getUrlEncoder().withoutPadding()
.encodeToString("Hello, World!".getBytes());# Encode a string
echo -n 'Hello, World!' | base64
# SGVsbG8sIFdvcmxkIQ==
# Decode a string
echo 'SGVsbG8sIFdvcmxkIQ==' | base64 --decode
# Hello, World!
# Encode a file
base64 photo.jpg > photo.b64
# Decode a file
base64 --decode photo.b64 > output.jpg
# Pipe from curl and decode
curl -s https://api.example.com/data | jq -r '.content' | base64 --decode# Encode
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("Hello, World!"))
# SGVsbG8sIFdvcmxkIQ==
# Decode
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($encoded))
# Hello, World!Here are the real-world debugging scenarios you'll actually run into.
Three likely causes:
- with + and _ with /, add padding if needed, try again.If you get an "invalid padding" error, try:
= signs until the string length is a multiple of 4= signs (some decoders handle this automatically)// Quick fix for padding issues
function fixBase64Padding(str) {
const pad = str.length % 4;
if (pad === 2) return str + '==';
if (pad === 3) return str + '=';
return str;
}MIME Base64 wraps lines at 76 characters. Some encoders add \n every 76 characters. Strip them before decoding:
const cleaned = base64String.replace(/[\n\r\s]/g, '');
const decoded = atob(cleaned);btoa in the browser only handles Latin-1 characters (code points 0-255). If your string contains emoji, Chinese characters, or anything outside that range, it'll throw InvalidCharacterError. Use the TextEncoder approach from the JavaScript section above.
Converting images to Base64 data URIs is one of the most common (and most abused) Base64 use cases.
Here's a quick rule of thumb: if your Base64 string is longer than about 20 lines when printed, you should probably just use a regular image file.
I need to say this one more time, louder, for the people in the back:
Base64 is not encryption. Base64 is not encryption. Base64. Is. Not. Encryption.
I have personally reviewed codebases where passwords were stored in Base64 "for security." I've seen API keys "protected" by Base64 encoding. I've watched junior developers argue that their data is safe because "it's encoded."
Base64 provides zero security. It's as reversible as ROT13 but somehow gets more undeserved trust. Here's the proof:
// "Securing" a password with Base64
const "encrypted" = btoa('MySecretPassword123');
// "TXlTZWNyZXRQYXNzd29yZDEyMw=="
// "Breaking" the "encryption"
atob('TXlTZWNyZXRQYXNzd29yZDEyMw==');
// "MySecretPassword123"
// Total time to "crack": 0 secondsIf you need actual security:
This is my PSA and I'm going to be blunt about it.
When you need to decode a Base64 string, your first instinct might be to Google "base64 decode online" and paste it into whatever shows up first. For random data, that's fine. But think about what you're decoding:
You have no idea what those sites do with the data you submit. Some log everything. Some are run by companies with questionable data practices. Some might be outright malicious.
Use your terminal. Use your browser's developer console. Use a tool that processes everything locally in your browser, where the data never leaves your machine.
There are browser-based Base64 tools that do all the encoding and decoding entirely in JavaScript — no server round-trip, no data transmission. Your input never leaves your browser tab. That's the kind of tool you want for anything even remotely sensitive.
| Property | Base64 | Base64URL | Hex | URL Encoding |
|---|---|---|---|---|
| Size overhead | +33% | +33% | +100% | Variable |
| Characters used | A-Z, a-z, 0-9, +, / | A-Z, a-z, 0-9, -, _ | 0-9, A-F | Original + %XX |
| URL safe | No | Yes | Yes | Yes |
| Padding | = (required) | Usually omitted | None | None |
| Binary safe | Yes | Yes | Yes | Yes |
| Human readable | No | No | Somewhat | Yes |
| Reversible | Yes | Yes | Yes | Yes |
| Provides security | No | No | No | No |
| Common use | Email, data URIs | JWTs, URLs | Debugging, hashes | Query strings |
Here's a non-exhaustive list of places you'll encounter Base64 without necessarily realizing it:
~/.ssh/id_rsa.pub is Base64)<canvas>.toDataURL() returns a Base64 data URIBase64 is one of those fundamental pieces of internet infrastructure that's hiding in plain sight. Once you understand it, you'll start seeing it everywhere — in your HTTP headers, your API payloads, your email source, your security tokens.
The core ideas are simple:
When you need to encode or decode Base64, use tools you trust — preferably ones that process data locally in your browser. There are developer tool suites that include Base64 encoders, JWT decoders, URL encoders, and hash generators that all run entirely client-side, so your data never touches a server. That's the right way to handle it.
Now go decode that mysterious string in your API response. You know how it works now.