iKit
Tutorial · 11 min read ·

How to Encode and Decode Base64 — With Real Examples

Base64 turns binary into text so it can travel through JSON, URLs, and headers. Learn to encode and decode Base64 with real browser, CLI, and code examples.

How to Encode and Decode Base64 — With Real Examples

How to Encode and Decode Base64 — With Real Examples

Base64 keeps showing up in places where binary data has to travel over text-only channels: email attachments, data URIs, JWT headers, HTTP Basic Auth. If you've ever stared at a wall of letters ending in == and wondered what it meant, this guide walks through what Base64 actually is, how to encode and decode it in the browser, terminal, and code, and where it belongs in a real project.

What Base64 encoding actually does

Base64 is a way of representing arbitrary binary data using only 64 printable ASCII characters. It's defined in RFC 4648 and has been around since the early days of email, where message bodies were restricted to 7-bit ASCII. Anything with a non-printable byte — an image, a compressed file, a cryptographic key — would get mangled by old mail relays. Base64 was the workaround, and it stuck.

The 64-character alphabet

The standard alphabet is the uppercase letters A-Z, the lowercase letters a-z, the digits 0-9, and the two symbols + and /. That's 62 + 2 = 64 printable characters. A 65th character, =, is used for padding. Together they represent all possible 6-bit values (0 through 63).

The URL-safe variant from RFC 4648 §5 swaps + for - and / for _, so encoded strings can live in URLs, filenames, and HTTP headers without escaping.

How the encoding works, bit by bit

Base64 takes every 3 input bytes (24 bits) and slices them into 4 groups of 6 bits. Each 6-bit value — between 0 and 63 — becomes one character from the alphabet.

Here's what happens to the 3-byte input Man:

Input:   M          a          n
ASCII:   77         97         110
Binary:  01001101   01100001   01101110
Re-group (6-bit): 010011  010110  000101  101110
Decimal:          19      22      5       46
Base64 char:      T       W       F       u
Output: "TWFu"

When the input length isn't a multiple of 3, the encoder pads with zero bits and appends = signs so the output stays aligned to 4 characters. M alone encodes to TQ==, and Ma encodes to TWE=.

Base64 is not encryption

It's worth repeating because the mistake is so common: Base64 is reversible by anyone, with no key. Treat it as a transport format, nothing more. If you need to protect data, use real encryption. If you need to verify integrity, use a hash — the iKit Hash Generator produces SHA-256 and SHA-512 digests client-side.

Encoding and decoding Base64 in the browser

Every modern browser ships with two global functions: btoa (binary to ASCII, i.e. encode) and atob (ASCII to binary, i.e. decode). They work for plain ASCII text without ceremony:

const encoded = btoa("Hello, world!");
console.log(encoded);
// "SGVsbG8sIHdvcmxkIQ=="

const decoded = atob(encoded);
console.log(decoded);
// "Hello, world!"

The Unicode trap

btoa only accepts characters in the Latin-1 range (code points 0-255). Pass in anything multi-byte and you'll get InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range. To encode arbitrary Unicode safely, go through TextEncoder and treat the bytes yourself:

function encodeUtf8Base64(str) {
  const bytes = new TextEncoder().encode(str);
  let binary = "";
  bytes.forEach(b => (binary += String.fromCharCode(b)));
  return btoa(binary);
}

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

encodeUtf8Base64("こんにちは");
// "44GT44KT44Gr44Gh44Gv"

The MDN reference on Base64 covers the edge cases in more detail, including the newer Uint8Array.fromBase64() method rolling out across browsers.

When you don't want to write the code

If you just need to paste a string and see the encoded (or decoded) version, skip the DevTools gymnastics and use the iKit Base64 Encoder. It runs entirely client-side — the bytes never leave your browser — and handles Unicode, URL-safe variants, and file uploads out of the box.

Encoding and decoding Base64 on the command line

The terminal is often the fastest path for one-off conversions, and it's scriptable enough to drop into a CI pipeline.

macOS and Linux

GNU coreutils ships a base64 command on almost every Unix system:

# Encode a string
echo -n "Hello, world!" | base64
# SGVsbG8sIHdvcmxkIQ==

# Decode a string
echo "SGVsbG8sIHdvcmxkIQ==" | base64 --decode
# Hello, world!

# Encode a file (useful for embedding images inline)
base64 < logo.png > logo.b64

# Decode back to the original binary file
base64 --decode < logo.b64 > logo.png

Watch out for the echo -n — without -n, echo appends a newline that gets encoded along with your input, producing the wrong output.

Windows PowerShell

PowerShell doesn't have a dedicated base64 cmdlet, but .NET's Convert class does the job:

# Encode
$bytes = [System.Text.Encoding]::UTF8.GetBytes("Hello, world!")
[Convert]::ToBase64String($bytes)
# SGVsbG8sIHdvcmxkIQ==

# Decode
[System.Text.Encoding]::UTF8.GetString(
  [Convert]::FromBase64String("SGVsbG8sIHdvcmxkIQ==")
)
# Hello, world!

OpenSSL as a portable fallback

If you're on a stripped-down container without base64 in PATH, OpenSSL is almost always installed and speaks Base64 natively:

echo -n "Hello, world!" | openssl base64
# SGVsbG8sIHdvcmxkIQ==

echo "SGVsbG8sIHdvcmxkIQ==" | openssl base64 -d
# Hello, world!

Encoding and decoding Base64 in code

Most languages expose Base64 in their standard library. Here's the minimum you need in the three most common back-end ecosystems.

Python

The base64 module in the standard library is your go-to. It returns bytes, so you'll typically decode to a UTF-8 string at the end:

import base64

# Standard Base64
base64.b64encode(b"Hello, world!").decode()
# 'SGVsbG8sIHdvcmxkIQ=='

base64.b64decode("SGVsbG8sIHdvcmxkIQ==").decode()
# 'Hello, world!'

# URL-safe variant, used by JWT and most OAuth flows
base64.urlsafe_b64encode(b"<<>>??").decode()
# 'PDw-Pj8_'

Node.js

Node's Buffer handles both directions without importing anything:

// Encode
Buffer.from("Hello, world!").toString("base64");
// 'SGVsbG8sIHdvcmxkIQ=='

// Decode
Buffer.from("SGVsbG8sIHdvcmxkIQ==", "base64").toString("utf8");
// 'Hello, world!'

// URL-safe (manually stripped)
Buffer.from("Hello, world!")
  .toString("base64")
  .replace(/\+/g, "-")
  .replace(/\//g, "_")
  .replace(/=+$/, "");

PHP

PHP has one-line functions built into core:

echo base64_encode("Hello, world!");
// SGVsbG8sIHdvcmxkIQ==

echo base64_decode("SGVsbG8sIHdvcmxkIQ==");
// Hello, world!

Five real-world examples of Base64

Theoretical encoding is fine; knowing where you'll actually see it matters more. Here are the five most common places.

1. Data URIs for inline images and fonts

Data URIs let you embed an image directly in HTML or CSS, trading an HTTP request for a larger payload. The format is data:<mime>;base64,<encoded>:

<img alt="1×1 red pixel"
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/AAAZ4gk3AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=" />

Useful for tiny assets (<2 KB), favicons, and email templates where external hosting is unreliable. For larger images, compress them first and host them normally; see the iKit Image Compressor for lossless PNG and WebP compression.

2. HTTP Basic Authentication

The Authorization: Basic header is Base64 of username:password. It's not encrypted — it's only obscured — which is why Basic Auth only belongs on HTTPS connections:

Authorization: Basic dXNlcjpwYXNzd29yZA==

Decode with base64 -d and you're back to user:password. If you need to generate random, SSL-safe API keys instead of hand-typing passwords, the iKit Password Generator creates them client-side using crypto.getRandomValues.

3. JWT tokens

A JSON Web Token is three URL-safe Base64 segments separated by dots: header.payload.signature. The first two segments are plain JSON, Base64-encoded without padding:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIn0
.
QTbSftRTuZ1PZTxhmvAMcXYxk5P7yoYTtv2T7_LlOYg

Decode the first segment with base64 -d (after adding back any missing = padding) and you get {"alg":"HS256","typ":"JWT"}. The signature is the only part that actually protects the token — the other two are fully readable. This is why you should never put secrets in a JWT payload.

4. Email attachments (MIME)

Every attachment you send over SMTP is Base64-encoded. Mail servers originally only accepted 7-bit ASCII, and that legacy lives on. If you view the raw source of an email with an attached PDF, you'll see something like:

Content-Type: application/pdf; name="invoice.pdf"
Content-Transfer-Encoding: base64

JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9GaWx0ZXIvRmxhdGVEZWNvZGUv
TGVuZ3RoIDE1Nj4+c3RyZWFtCnic...

The lines wrap at 76 characters per RFC 2045 convention, which is why MIME-encoded blocks look like narrow columns rather than one long string.

5. PEM-encoded certificates and keys

TLS certificates, SSH keys, and most cryptographic material ship as PEM — a human-readable wrapper around Base64-encoded DER binary. You've seen them before:

-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEagZDbTANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJV
UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEV
...
-----END CERTIFICATE-----

Strip the header, footer, and newlines; decode the remainder; and you get the DER-encoded X.509 structure. Tools like openssl x509 -text -in cert.pem do that automatically.

Common mistakes to avoid

A few patterns trip up developers again and again.

Mistake Why it fails Fix
Treating Base64 as a secret Anyone can decode it in one command Use AES-GCM or libsodium for secrets
Using standard alphabet in URLs + and / get URL-encoded and break Use URL-safe Base64 (- and _)
Forgetting padding when concatenating Decoders misalign on 4-byte groups Strip padding before joining, re-pad before decoding
btoa on Unicode strings Throws on characters above code point 255 Go through TextEncoder first
Encoding echo "text" in bash Includes a trailing newline Use echo -n or printf

When Base64 is the wrong choice

Base64 makes sense when you genuinely need to embed binary in a text-only medium. If you're shipping data between two services that both speak binary (gRPC, Protobuf, MessagePack), skipping Base64 is a 33% size win and a non-trivial CPU saving at scale. Reach for it by default only when one of these is true:

  • The transport is text-only (email, JSON over HTTP, URLs, YAML).
  • The payload is small (< a few hundred KB).
  • Human readability or copy-pasting matters.
  • You're matching an existing standard that mandates it (JWT, PEM, MIME, Basic Auth).

Padding and line-wrapping gotchas

Some producers strip = padding; some wrap lines at 64 or 76 characters; some do neither. Before decoding, normalise the input:

import base64

def safe_b64decode(s: str) -> bytes:
    # Remove whitespace/newlines and re-add missing padding
    s = "".join(s.split())
    missing = -len(s) % 4
    return base64.b64decode(s + "=" * missing)

safe_b64decode("SGVsbG8sIHdvcmxkIQ")  # works without padding
# b'Hello, world!'

Most libraries are strict by default — add defensive padding at your ingestion layer if you're consuming Base64 from third parties.

A quick mental checklist

Before you reach for Base64, ask these five questions. If you answer "yes" to any, it's probably the right choice:

  • Does this data need to pass through a text-only channel (email, JSON, URL, header)?
  • Is the payload small enough that a 33% size inflation is acceptable?
  • Do I need human-readable or copy-pasteable encoding?
  • Am I implementing a spec that requires Base64 (JWT, PEM, MIME, Basic Auth, Data URI)?
  • Do I need to embed binary directly in source code or config?

If none of those apply, you're probably better off with raw bytes, gzip, or a proper binary serialization format.

Related on iKit

Related posts