Once you understand that Base64 is just "bytes dressed up as text," a lot of everyday engineering patterns suddenly make sense. This article walks through seven places you will meet Base64 in real systems, with the actual code or config for each and the gotcha that bites in each one. After the seven, I will spend a section on when you should explicitly refuse to reach for Base64, because the encoding has costs that are easy to ignore until they show up in a bill or a breach.
1. JWTs: three Base64url segments
A JSON Web Token is three dot-separated parts: header.payload.signature. The first two are Base64url-encoded JSON objects; the third is a signature over them. Decoding the middle segment reveals everything the token carries, which is exactly why you must never treat a JWT payload as private.
The encoding step is trivial and runs in any language:
const [, payload] = token.split('.');
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const data = JSON.parse(atob(b64));
console.log(data.sub, data.exp);
Why Base64url and not plain Base64 here? Because the token travels through URLs, headers, and cookies. The standard alphabet contains + and /, both of which carry special meaning in those contexts, so JWTs swap them for - and _ and usually drop the padding =. The same data, just a URL-safe skin. A nice side effect: the token stays safe through a URL round trip without extra escaping.
Trap: because the payload is only encoded, anyone holding the token can read it. A token logged in an access log or copied into a support ticket leaks its claims in plaintext. Keep the payload to identifiers and flags; put nothing secret there.
2. Data URIs: inlining small assets
Instead of a separate HTTP request for a tiny icon or a one-pixel tracker, you can embed it directly in HTML or CSS as a data URI:
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==" alt="icon">
The browser decodes the Base64 into the image without a network round trip. For a favicon or a spinner that is a genuine win: fewer requests, no extra DNS lookups, and the asset cannot 404.
Trap: Base64 inflates by about 33%, and the browser cannot cache an inlined asset separately from the document that contains it. Inlining is great for a 200-byte icon; it is a mistake for a 50 KB photo, and a disaster for a 2 MB video. If you inline everything, your HTML page itself becomes the bottleneck, and every change to one icon forces the whole page to re-download. The rule of thumb: inline below roughly 1-2 KB, link above that.
3. Email attachments and MIME
SMTP was specified to move 7-bit ASCII text. Raw bytes of a PDF or a PNG contain characters that SMTP would mangle or drop, so mail clients Base64-encode attachments and label the part:
Content-Type: application/pdf
Content-Transfer-Encoding: base64
JVBERi0xLjQKMSAwIG9iago8PAovVGl0bGUgKEV4YW1wbGUpCj4+Cg==
The receiving client decodes the block back into the original bytes. The same mechanism appears in multipart form posts and in some message-queue payloads that only accept text.
Trap: the MIME variant inserts a line break every 76 characters. A strict decoder that rejects whitespace will throw on a real email body. When you decode mail or PEM content, strip all whitespace first, or use a library that already understands the MIME wrapping. Also remember the 33% tax applies to every attachment, so a 10 MB PDF becomes about 13 MB on the wire and costs that much more storage on every recipient.
4. HTTP Basic Auth
Basic Auth sends credentials in a header. The format is username:password, Base64-encoded, prefixed with Basic :
const creds = Buffer.from('alice:s3cret-pw').toString('base64');
fetch('https://api.example.com', {
headers: { Authorization: 'Basic ' + creds }
});
The server decodes the header, splits on the colon, and checks the pair. Simple and dependency-free, which is why you still see it in internal tools and quick prototypes.
Trap, and this one is serious: the encoding is not encryption. Anyone who can read the request - a proxy, a logging layer, the browser devtools, a colleague behind the same NAT - can decode alice:s3cret-pw instantly. Basic Auth over plain HTTP is essentially sending the password in the clear. Always pair it with TLS, and prefer token or OAuth flows where you can, because a Base64 credential in a log line is a credential leak.
5. Binary blobs in JSON and XML
JSON and XML are text formats. If you want to carry a thumbnail, a signature, or a protobuf payload inside them, you Base64 the bytes into a string field:
{
"id": "img-42",
"name": "logo",
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
This is how databases such as Postgres return bytea over a text protocol, and how many APIs ship small binaries. The alternative - escaping raw bytes - is far more error prone.
Trap: the 33% growth hits your payload size, your parse time, and your memory. A 5 MB file sent as a JSON string field becomes about 6.7 MB and must be fully buffered in memory to decode. For anything large, prefer a binary transport (gRPC, a multipart upload, or a presigned URL to object storage) and keep Base64 for the small stuff. As a sanity check, if your JSON response is mostly one giant Base64 string, you are probably abusing the format.
6. API request signing and webhooks
Many signing schemes return a digest that you then Base64 to ship inside a header. HMAC-SHA256 over a webhook body is the classic example:
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const sig = hmac.digest('base64');
// send header: X-Signature: sig
The receiver recomputes the HMAC and compares. Base64 lets a fixed-length, byte-oriented digest travel as a header value without breaking the text protocol. The same idea backs AWS SigV4 and GitHub webhook signatures.
Trap: comparison must be constant-time. A naive === on the decoded bytes leaks timing information an attacker can use to forge a signature byte by byte. Use crypto.timingSafeEqual or its equivalent, and compare after decoding both sides from Base64 rather than comparing the strings directly, because the padding and alphabet allow multiple text representations of the same digest and a string compare can mismatch on two valid encodings.
7. Kubernetes Secrets and ConfigMaps
In Kubernetes, Secret values are stored Base64-encoded in the manifest:
apiVersion: v1
kind: Secret
metadata:
name: db
type: Opaque
data:
password: czNjcmV0LXB3Cg==
The data field requires Base64; the friendlier stringData field lets you write plaintext and the API server encodes it for you. ConfigMaps use plaintext, which is a common source of confusion between the two object types.
Trap: this encoding is a formatting requirement, not a security control. Anyone with kubectl get secret -o yaml can decode the password in one line. Do not mistake a Secret for encryption at rest unless you have enabled etcd encryption. Also, because the value is in the manifest, it lands in Git if you commit YAML, so feed secrets from a real secret manager and reference them, never paste them into committed files.
When you should NOT use Base64
The seven uses above are legitimate because in each one the job is "carry bytes through text." The mistakes happen when people reach for Base64 to solve a problem it does not solve.
The 33% size tax is real
Every byte you encode becomes 4/3 of a character. At small scale nobody cares; at scale it adds up fast. A service that Base64-encodes 2 GB of blobs per hour ships an extra 660 MB of pure overhead, pays for it in bandwidth, and pays again in CPU to encode and decode. If your goal is to shrink data, the answer is compression (gzip, zstd, brotli), not Base64, which makes things larger. Reach for Base64 only when the channel demands text, never as a space-saving trick.
Base64 is not encryption
This bears repeating because it keeps biting teams. Encoding a password, an API key, or a token with Base64 provides zero confidentiality. The "incident of the month" is usually a developer who "obfuscated" a credential by Base64-ing it, committed it to a public repo, and called it protected. It is not. Use TLS in transit, AES-GCM or ChaCha20 for symmetric encryption, and bcrypt or argon2 for stored passwords. If the only thing between an attacker and your secret is Base64, you have no secret.
Base64 in logs is a leak surface
Because Base64 looks like noise, engineers forget it is readable. A token, a signed cookie, or a Basic credential that gets logged as Base64 is fully recoverable by whoever reads the log. Worse, attackers scan logs for long Base64 runs precisely because they often contain exactly those secrets. Redact or hash sensitive fields before they reach logs, and treat any Base64 string longer than a few dozen characters in a log as suspect. A good log pipeline should mask known secret fields rather than rely on the encoding to hide them.
A quick decision guide
Use Base64 when the transport or format is text-only and you must carry bytes: tokens, data URIs, mail, headers, small JSON blobs. Do not use it to hide data, to save space, or to protect credentials. Keep secrets out of payloads, out of manifests, and out of logs, and let real cryptography do the protecting. When you are ready to experiment, paste some text into the Base64 tool and toggle the URL-safe option to watch the alphabet change.