ToolSink
Back to blog

Base64 Encoding and Decoding: How It Works, With Code You Can Actually Run

ToolSink Team

Base64 Encoding and Decoding: How It Works, With Code You Can Actually Run

If you've ever opened a JWT, an inline image in CSS, or an HTTP Basic Auth header, you've run into a string that ends in = and looks like noise. That's Base64 — and the fastest way to actually understand it is to encode and decode something yourself rather than read about it in the abstract.

This post covers the mechanics briefly, then gets straight into runnable code and the mistakes that actually trip people up in real projects.

The short version

Base64 turns arbitrary binary bytes into a string made up of only 64 safe characters (A–Z, a–z, 0–9, +, /), so that data can survive being sent through systems built for plain text — email (SMTP), URLs, JSON, XML. It is not encryption. Anyone can decode it instantly; it provides zero confidentiality.

Base64Encryption (e.g. AES)
PurposeSafe transport of binary in text systemsConfidentiality
Reversible without a key?Yes, alwaysNo
Adds size~33%Depends on cipher/mode
Use for passwords?NeverYes, appropriately

Try it yourself: encode "Hi" by hand

  • H = 72 = 01001000, i = 105 = 01101001
  • Combined: 0100100001101001
  • Split into 6-bit groups: 010010 000110 1001 → pad the last group to 100100
  • Look up each value (18, 6, 36) in the Base64 alphabet: S, G, k
  • Since the input was 2 bytes (not a multiple of 3), pad the output to a multiple of 4: SGk=

Decoding reverses this: strip the =, map each character back to its 6-bit value, regroup into 8-bit bytes, read as ASCII. SGk=Hi.

You don't need to do this by hand in real work — here's the same operation in three languages.

Code: encode and decode in the wild

JavaScript (browser)

// Encode
const encoded = btoa("Hi"); // "SGk="

// Decode
const decoded = atob("SGk="); // "Hi"

// btoa/atob only handle Latin1 — see the Unicode pitfall below

JavaScript (Node.js)

const encoded = Buffer.from("Hi", "utf-8").toString("base64"); // "SGk="
const decoded = Buffer.from("SGk=", "base64").toString("utf-8"); // "Hi"

Python

import base64

encoded = base64.b64encode(b"Hi").decode()  # 'SGk='
decoded = base64.b64decode("SGk=").decode()  # 'Hi'

Command line (cURL/bash)

echo -n "Hi" | base64          # SGk=
echo -n "SGk=" | base64 -d     # Hi

Where Base64 actually shows up

  • MIME email attachments — SMTP only guarantees 7-bit ASCII, so images and files get Base64-encoded before sending.
  • Data URIs — small icons/fonts embedded directly in CSS/HTML (data:image/png;base64,...) to cut HTTP requests.
  • JWTs — the header and payload segments of a JSON Web Token are Base64URL-encoded JSON (not encrypted — never put secrets in a JWT payload assuming it's hidden).
  • HTTP Basic AuthAuthorization: Basic <base64(user:pass)>. This is not secure on its own; it relies entirely on HTTPS to hide the credentials in transit.
  • Storing binary in text-only fields — some JSON columns, config files, or legacy APIs that can't accept raw binary.

Three mistakes that actually cause bugs

1. Assuming btoa()/atob() handle Unicode. btoa("café") throws an error in the browser, because btoa only supports Latin1 (single-byte) characters. The fix is to UTF-8 encode first:

const encoded = btoa(unescape(encodeURIComponent("café")));
const decoded = decodeURIComponent(escape(atob(encoded)));

Node's Buffer doesn't have this problem — it handles UTF-8 natively, which is one reason server-side Base64 work is less error-prone than doing it in the browser.

2. Using standard Base64 in a URL. Standard Base64's + and / characters break query strings and file paths. Use Base64URL instead, which swaps +- and /_, and typically drops the = padding:

import base64
url_safe = base64.urlsafe_b64encode(b"Hi").decode()

3. Treating Base64 as a security measure. Base64-encoding a password or API key before storing or logging it does not protect it — it's reversible in one line of code by anyone who sees it. If you need confidentiality, encrypt with AES or similar and manage the key properly; Base64 is not a substitute step in that process.

The trade-off to know about

Base64 inflates data size by roughly 33% (it encodes 3 bytes of input as 4 bytes of output). That's a fine cost for a 2KB icon inlined into CSS. It's a bad idea for a 2MB hero image — you'll bloat your CSS file and hurt page load for no real benefit. As a rule of thumb: inline small, static, frequently-reused assets; link everything else.

Quick check without writing code

If you just need to decode or encode something once — a JWT payload, a Data URI, an auth header — pasting it into a formatter is faster than spinning up a script. ToolSink's Base64 Encode/Decode tool runs entirely in your browser, so nothing gets sent to a server.