·6 min read

Base64 Encoding Explained: What It's For (and What It Isn't)

Base64 shows up in auth headers, JWTs, data URIs and API payloads. It's simple once you see the 3-bytes-to-4-characters trick, and most of the bugs come from three predictable places.

Ram profile

Ram

Base64 encoding explained with developer examples
Share

Base64 is one of those things you use for years before you think about it. It's in the Authorization: Basic header, in every JWT, in data:image/png;base64,... URLs, and in API responses that need to carry a file inside JSON.

The idea is simple, and most of the bugs it causes come from the same three places. If you just need to convert something, the Base64 encoder/decoder runs in your browser.

Why it exists

A lot of older protocols, email being the classic example, were built to carry text. Binary data (images, PDFs, anything) contains byte values that those systems treat as control characters: line endings, null bytes and so on. Push raw binary through and it gets mangled.

Base64 solves this by rewriting any bytes using only 64 safe, printable characters: A–Z, a–z, 0–9, + and /. It is a transport format, nothing more.

How 3 bytes become 4 characters

Base64 works in groups of three bytes, which is 24 bits. It splits those 24 bits into four 6-bit chunks. Each 6-bit chunk is a number from 0 to 63, which maps to one character.

Take the text Man:

M        a        n
01001101 01100001 01101110      ← 24 bits
010011 010110 000101 101110     ← four 6-bit groups
T      W      F      u          ← "TWFu"

Two consequences:

  • It's about 33% bigger. Every 3 bytes become 4 characters. A 3 MB image turns into roughly 4 MB of text.
  • Padding. If the input isn't a multiple of three bytes, the output is padded with = to a multiple of four characters. Ma becomes TWE= and M becomes TQ==.

Where you'll run into it

HTTP Basic auth. The header is just username:password in Base64:

Authorization: Basic cmFtOnNlY3JldDEyMw==    →    ram:secret123

Paste it into a decoder and the password is right there. That's why Basic auth is only acceptable over HTTPS.

JWTs. The header and payload are Base64URL-encoded JSON. That's why anyone can read a JWT payload. The JWT post covers why that's fine, and when it isn't.

Data URIs. You can embed a small image directly in HTML or CSS:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." />

This is worth it for tiny icons. For anything larger you lose browser caching and pay the 33% size penalty, so a normal image file is usually better.

Files inside JSON. JSON can't hold raw bytes, so APIs that return a file inside a JSON body encode it:

{ "filename": "invoice.pdf", "content": "JVBERi0xLjQKJcOkw7z..." }

That's fine for small files. For large ones, return a download URL or a binary response instead. Base64 makes the payload a third bigger, and the client has to decode all of it before it can use any of it.

Email attachments. MIME encodes attachments as Base64, wrapped at 76 characters per line. You only notice this when you're debugging raw email source.

The three bugs it causes

1. Treating it as security

Base64 is not encryption. There's no key; anyone can reverse it in one line. If you ever see something like this used to "protect" a value:

const token = btoa(JSON.stringify({ userId: 1, role: "admin" }));
// eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9

…then a user can decode it, change role, encode it again and send it back. If a server makes access decisions from a value like that, it's a real vulnerability. Use a signed token (a JWT, verified properly) or encrypt it with an actual crypto library.

The same goes for passwords. Store them hashed with bcrypt or Argon2, never encoded.

2. Putting standard Base64 in a URL

+, / and = all mean something in URLs. A + in a query string is usually read as a space, so your token arrives corrupted and decoding fails in confusing ways.

There are two fixes:

// Option 1: percent-encode it
const url = `/api/resource?token=${encodeURIComponent(b64)}`;

// Option 2: use Base64URL (- instead of +, _ instead of /, no padding)
const b64url = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

In Node.js, Buffer.from(data).toString("base64url") gives you Base64URL directly. JWTs and OAuth PKCE codes use Base64URL for exactly this reason.

3. btoa() with non-English text

The browser's btoa() only accepts characters in the Latin-1 range. Give it Hindi, Telugu or an emoji and it throws:

btoa("नमस्ते");
// InvalidCharacterError: ...contains characters outside of the Latin1 range

The fix is to turn the string into UTF-8 bytes first:

function toBase64(str) {
  const bytes = new TextEncoder().encode(str);
  let binary = "";
  for (const b of bytes) binary += String.fromCharCode(b);
  return btoa(binary);
}

function fromBase64(b64) {
  const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

I've used a loop instead of String.fromCharCode(...bytes) because spreading a large array into function arguments can overflow the call stack on big inputs.

Newer browsers also have Uint8Array.prototype.toBase64() and Uint8Array.fromBase64(), which handle all of this for you. Check browser support before relying on them.

Node.js doesn't have this problem: Buffer.from(str, "utf8").toString("base64") handles Unicode correctly.

Quick reference

Node.js

Buffer.from("Hello").toString("base64");          // "SGVsbG8="
Buffer.from("SGVsbG8=", "base64").toString();     // "Hello"
Buffer.from("Hello").toString("base64url");       // URL-safe

Python

import base64
base64.b64encode(b"Hello")             # b'SGVsbG8='
base64.b64decode(b"SGVsbG8=")          # b'Hello'
base64.urlsafe_b64encode(b"Hello")     # URL-safe

Should I use Base64 here?

Situation Answer
Small icon inline in CSS Fine
Large image or file in an API response No. Return a URL or a binary response
HTTP Basic auth Required by the protocol (use HTTPS)
Value in a URL Base64URL, or percent-encode it
Hiding a password or secret No. Hash or encrypt it

A quick way to recognise it

A Base64 string only contains A–Z a–z 0–9 + /, sometimes ends in one or two =, and has a length that's a multiple of four. If it has - and _ instead, it's Base64URL. If it starts with eyJ, it's almost certainly JSON. {" encodes to eyJ, which is why every JWT starts that way.

Share

Related Articles

How to merge PDF files without uploading them to any server
Tech··6 min read

How to Merge PDFs Without Uploading Them Anywhere

Loan applications and visa forms always want one PDF made of ten. Here's how to merge them without sending your bank statements to someone else's server, and how to verify that nothing was uploaded.

Ram profileRam

Stay in the loop

Get the latest articles delivered straight to your inbox. No spam, ever.