Skip to content
Base64 in Production — When It Helps and When It Hurts

Encoding

Base64 in Production — When It Helps and When It Hurts

Base64 is a transport encoding, not compression and not encryption. When it earns its 33% overhead in real systems, and when it is a code smell to rip out.

Base64 pays for itself in a handful of specific situations and is a code smell everywhere else. Engineers reach for it out of habit when they hit anything binary, then pay the 33% size penalty and the readability tax forever. This guide is the decision tree.

What Base64 actually is

Base64 maps every 3 bytes of input (24 bits) to 4 output characters (6 bits each) drawn from a 64-character alphabet: A-Z, a-z, 0-9, and two symbols. Standard Base64 uses + and /; URL-safe Base64 (RFC 4648 §5) uses - and _. Padding with = brings the output length to a multiple of 4.

That is the whole spec. It is not compression (output is 4/3 the input size, a +33% overhead before you count padding). It is not encryption (the decode is public). It is a transport encoding: a way to move bytes through a channel that only safely carries text.

VariantAlphabetPaddingUsed in
Standard Base64 (RFC 4648 §4)A-Z a-z 0-9 + /=MIME, PEM, most APIs
URL-safe (RFC 4648 §5)A-Z a-z 0-9 - _often omittedJWT, URL tokens
Base64url no paddingsame as §5noneJWT base64url(payload)

The URL-safe flavor exists because + and / have meaning in URLs (+ is a space in form encoding, / is a path separator). See the URL encoding guide for the rules that make this matter.

The cost line

Before adopting Base64 anywhere, price it:

  • Size: ceil(n / 3) * 4 bytes for n input bytes. A 1 MB file becomes 1.37 MB on the wire. Gzip recovers most of it for text-like payloads, but you are still burning CPU at both ends.
  • CPU: Negligible on modern hardware for small blobs. Real for megabyte-per-request throughput.
  • Readability: A log line with 4 KB of Base64 is a log line you cannot grep.
// Quick sanity check for overhead
const bytes = new Uint8Array(1_000_000);
const b64 = btoa(String.fromCharCode(...bytes));
console.log(b64.length); // 1_333_336 — exactly 4/3 + padding

To experiment with the encode and decode steps directly, the text-to-binary and binary-to-text tools let you see the byte layout behind the scenes.

Where Base64 earns its keep

Four cases where Base64 is correct:

  1. Inlining small binary assets into text documents. CSS url(data:image/png;base64,...) for favicons, email signatures, PDFs with embedded images. Under 2-4 KB the extra HTTP round-trip beats the 33% overhead.
  2. JSON or XML payloads that must carry bytes. JSON has no binary type. Base64 in a string field is the canonical escape hatch. Alternatives (multipart, separate upload URL) are better when payloads grow past a few kilobytes.
  3. PEM-wrapped cryptographic keys and certificates. -----BEGIN CERTIFICATE----- followed by Base64 of DER bytes is the universal format for X.509 and PKCS material.
  4. JWT payloads. Header, claims, and signature are each base64url-encoded without padding. That is not negotiable: the spec is written that way.

Where Base64 hurts

  • Storing binary in a relational database as a TEXT column. Your database already has BYTEA, BLOB, VARBINARY. Using them saves 25%, preserves indexability of hashes, and avoids an encode/decode round on every read.
  • Base64 in URLs of unbounded size. Browsers cap URLs around 2000-8000 chars depending on the stack. Base64 a 10 KB blob into a query string and you will hit it.
  • “Obfuscating” secrets in a config file. Base64 is not a secret. Anyone can decode echo ZHJvd3NzYXA= | base64 -d. If you need a secret, use a secret manager.
  • Serializing UUIDs for URLs. A UUID is already 36 hex characters. Base64url trims it to 22 but the readability loss almost never justifies the saving. See the UUID v4 vs v7 piece for the primary-key discussion.

Data URIs — the one detail that catches teams

Data URIs embed Base64 (or plain percent-encoded) bytes inside a URL: data:[<mediatype>][;base64],<data>. Three rules:

  • Chrome and Firefox no longer allow data URIs for top-level navigation; they were a common phishing vector. They remain fine for <img>, CSS url(), and <iframe> with the caveats below.
  • Content Security Policy blocks data: sources by default. You need img-src data: or similar to re-enable.
  • Mobile browsers often cap data URI length around 2 MB, but performance falls off a cliff much earlier.
<!-- Good: tiny inline icon, under 1 KB -->
<img src="data:image/svg+xml;base64,PHN2Zy4uLg==" alt="">

<!-- Bad: inlining a 500 KB hero image -->
<img src="data:image/png;base64,iVBORw0KGgo...">

URL-safe vs standard — the bug you will eventually hit

A subtle trap: a library that defaults to standard Base64 produces output with + and /, which then arrives on a server that expects URL-safe Base64. Two classic failure modes:

  • The + character arrives decoded as a space because the server ran decodeURIComponent on the query string before Base64-decoding.
  • Padding = gets percent-encoded to %3D, the decoder does not strip it, and length validation fails.

Fix: pick one variant per interface and document it. For anything that touches URLs, JWTs, or cookies, use URL-safe without padding and normalize on receive. For MIME and PEM, stay on standard.

Performance notes

A rough mental model for when encode/decode cost matters in practice:

  • Under 10 KB: effectively free on any CPU from the last decade. Do not optimize.
  • 10 KB to 1 MB: microseconds to low milliseconds per operation. Still negligible for most web requests.
  • 1 MB to 100 MB: now you can measure it. A 100 MB payload encoded in Node’s Buffer.from(...).toString("base64") costs tens of milliseconds. If you are doing this in a hot path, consider streaming or avoiding the encode entirely.
  • Above 100 MB: Base64 is almost always the wrong answer. Use a signed URL, a multipart upload, or a side channel.

One more sharp edge worth calling out: atob and btoa in browsers work on binary strings (one-char-per-byte), not on Uint8Array. Calling btoa("é") throws InvalidCharacterError because é is two bytes in UTF-8 but btoa expects Latin-1 semantics. The correct modern approach is Uint8Array.prototype.toBase64() (Baseline 2024) or, as a fallback, encoding through a TextEncoder first.

Takeaways

Use Base64 when you are stuffing bytes through a text-only channel (JSON, URLs, PEM, email bodies). Do not use it as a database type, as obfuscation, or as a general-purpose “binary-to-string” hammer. For the encoding cousins that come up in the same conversations, see the character encoding guide and the URL encoding rules.

Related tools

By ·