# Base64 explained: what it is, and when to use it

> Base64 encodes binary data as 64 safe ASCII characters, making it about 33% larger. It is an encoding, not encryption - anyone can decode it instantly. Use it only where binary cannot travel: data URIs, email attachments, and JWT segments.

Source: https://rankcert.com/blog/base64-encoding-explained
Published: 2026-08-29 · Updated: 2026-08-29

---


Base64 turns arbitrary binary data into text using 64 characters that survive any transport: `A–Z`, `a–z`, `0–9`, `+` and `/`, with `=` as padding.

## How it works

Take three bytes - 24 bits. Split them into four groups of six bits. Each 6-bit group indexes into the 64-character alphabet.

```
Input:   M         a         n
Bits:    01001101  01100001  01101110
Regroup: 010011  010110  000101  101110
Output:  T       W       F       u
```

Three bytes in, four characters out. That ratio is the whole story: **base64 is always 33% larger than the input**, plus padding.

When the input length is not a multiple of three, the last group is padded with `=` so the output length stays a multiple of four.

Free tool: [Base64 Encode / Decode](https://rankcert.com/tools/base64-encode-decode) - Encode text to base64 or decode it back, with full UTF-8 support and automatic handling of base64url input. Runs entirely in your browser.

## base64 vs base64url

The standard alphabet uses `+` and `/`, both of which have meaning in URLs - `+` can be read as a space, `/` as a path separator.

base64url swaps them: `+` becomes `-`, `/` becomes `_`, and the trailing `=` padding is usually dropped entirely.

This is why decoding a JWT with a plain base64 decoder sometimes fails. JWTs use base64url. The fix is two replacements and re-adding padding:

```js
const decode = (segment) => {
  const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
  return atob(padded + "=".repeat((4 - (padded.length % 4)) % 4));
};
```

## The UTF-8 trap in JavaScript

`btoa` and `atob` operate on binary strings, one byte per character. Give `btoa` anything outside Latin-1 and it throws; `atob` on multi-byte content returns mojibake.

Correct round trip:

```js
const encode = (text) =>
  btoa(String.fromCharCode(...new TextEncoder().encode(text)));

const decode = (b64) =>
  new TextDecoder().decode(
    Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)),
  );
```

Skip the `TextEncoder` step and every accented character, emoji and non-Latin script breaks.

## Base64 is not encryption

It provides zero confidentiality. Anyone can decode it in one line, and every browser has a decoder built in.

Things it does not do: hide an API key in your bundle, protect a config value, or make a token safe to log. If you are reaching for base64 to conceal something, you want encryption instead.

## When inlining is worth it

Base64 in a data URI removes a network request at the cost of 33% more bytes and no caching.

**Worth it:** tiny assets under about 2KB - a small icon, a placeholder blur - where a round trip costs more than the extra bytes.

**Not worth it:** anything larger. A 100KB image becomes 133KB, cannot be cached separately, blocks the CSS or HTML it is embedded in from being parsed, and has to be re-downloaded whenever that file changes.

The number that decides it is round-trip latency versus transferred bytes. On HTTP/2 with many parallel requests, the round-trip cost collapsed and the case for inlining got much weaker than it was a decade ago.

<Callout>
If you are base64-encoding something to put in a JSON payload, check whether you can send it as a separate binary upload instead. You are paying a third more bandwidth for the convenience of one request.
</Callout>

<Cta />
