Base64 Encoding Explained

What it is, how it works, and when to use it in your projects

EncodingApril 25, 20267 min readBy Keyur Patel

You've seen those weird strings that end with one or two equals signs — SGVsbG8gV29ybGQ= — and maybe wondered what they are. That's Base64. It's not a password, it's not encryption, and it's definitely not random garbage. It's one of those things you'll use almost every day as a developer once you understand it.

I remember being confused by Base64 when I was starting out. Someone told me "just encode it" and I thought that meant it was secure. Nope. Base64 is about compatibility, not security. Let me explain the whole thing properly — why it exists, how it works under the hood, and the situations where you'll actually need it.

What Is Base64, Really?

Base64 takes any data — text, images, PDFs, whatever — and converts it into a string that only uses 64 "safe" characters: letters (A-Z, a-z), numbers (0-9), plus sign (+), and forward slash (/). The equals sign (=) shows up at the end as padding.

Why 64 characters? Because 64 is 2⁶ — meaning each character represents exactly 6 bits of data. That's the math behind the name. Not complicated once you see it.

Critical point that trips people up: Base64 is NOT encryption. I've seen production code where someone "secured" passwords by Base64 encoding them. Don't do this. Anyone can decode it instantly — there's no key, no secret. It's just a format change, like converting Celsius to Fahrenheit. The information is the same, just represented differently.

Why Does Base64 Even Exist?

Here's the problem it solves: back in the day, email systems only handled plain text. 7-bit ASCII. Try sending a JPEG through a system that only understands text and it'll get mangled — certain byte values get interpreted as control characters, newlines get inserted in wrong places, and your image arrives corrupted.

Base64 was the fix. Convert any binary data into characters that every text system handles safely, transmit it without corruption, then decode it back on the other end. Problem solved.

And that core use case — "I need to shove binary data through a text-only channel" — is still why we use Base64 today. JSON payloads, HTML data URIs, HTTP headers, email attachments. The specific systems changed, but the fundamental problem didn't.

How Base64 Encoding Works

The encoding process works in three steps:

  1. Take the input bytes and group them into sets of 3 bytes (24 bits).
  2. Split each 24-bit group into four 6-bit values.
  3. Map each 6-bit value to its corresponding Base64 character using the Base64 alphabet.

If the input length is not a multiple of 3, padding characters (=) are added to make the output length a multiple of 4.

Example: Encoding "Hi"

Input: H = 72 = 01001000, i = 105 = 01101001

Combined bits: 010010000110100100 (padded to 24 bits)

Split into 6-bit groups: 010010 000110 100100 000000

Mapped to Base64: S G k = (padding)

Result: SGk=

Real-World Use Cases

Embedding Images in HTML/CSS

Data URIs allow images to be embedded directly in HTML or CSS as Base64 strings, eliminating an HTTP request. Useful for small icons and inline SVGs.

src="data:image/png;base64,iVBORw0KGgo..."

API Authentication Headers

HTTP Basic Authentication encodes credentials as Base64 in the Authorization header. Note: this is not secure without HTTPS.

Authorization: Basic dXNlcjpwYXNzd29yZA==

Email Attachments (MIME)

Email protocols use Base64 to encode binary attachments so they can be safely transmitted as text through SMTP servers.

Content-Transfer-Encoding: base64

Storing Binary Data in JSON

JSON only supports text. When an API needs to include binary data (like a file or image) in a JSON payload, Base64 encoding is the standard approach.

{ "file": "SGVsbG8gV29ybGQ=" }

When Base64 is the Wrong Choice

I've reviewed codebases where Base64 was used for things it should never touch. Let me save you the embarrassment:

  • Don't use it for security. I can't stress this enough. Base64 is not encryption. I once found a production app that "protected" API keys by Base64 encoding them in the frontend. Anyone with DevTools could decode them in 2 seconds.
  • Don't use it for large files. Base64 makes everything 33% bigger. A 1MB image becomes 1.33MB. For a 10MB video? That's 3.3MB of wasted bandwidth. Use proper file upload endpoints instead.
  • Don't embed large images in CSS/HTML. Sure, tiny icons (under 5KB) are fine as data URIs. But I've seen people inline 200KB background images. Those can't be cached separately, can't be lazy-loaded, and bloat your HTML.

Base64 in Modern Web Development

Base64 encoding is woven into the fabric of modern web development in ways that are not always obvious. From CSS background images to authentication tokens, understanding where and how Base64 is used helps you work more effectively with the web platform.

Data URIs in CSS and HTML

Data URIs allow you to embed binary resources — images, fonts, SVGs — directly into CSS or HTML as Base64-encoded strings. This eliminates an HTTP request for small assets, which can improve performance for icons and inline images.

CSS Background Image as Data URI

.icon {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...");
  width: 24px;
  height: 24px;
}

The format is data:[mediatype];base64,[data]. For SVGs, you can often use URL-encoded SVG directly instead of Base64, which produces smaller output. Data URIs are best suited for small assets under 5KB — larger assets should be served as separate files to benefit from browser caching.

Base64URL in JWT Tokens

JWT tokens use a variant called Base64URL encoding, which is slightly different from standard Base64. Base64URL replaces + with - and / with _, and omits the = padding. This makes the encoded string safe to use directly in URLs and HTTP headers without additional percent-encoding.

Each of the three parts of a JWT (header, payload, signature) is Base64URL-encoded. This is why you can decode the header and payload of any JWT without a key — the encoding is not encryption. Only the signature requires the secret key to verify.

Base64 in Node.js with Buffer

Node.js provides the built-in Buffer class for working with Base64. It handles both encoding and decoding without any external dependencies:

// Encode a string to Base64
const encoded = Buffer.from("Hello, World!").toString("base64")
// → "SGVsbG8sIFdvcmxkIQ=="

// Decode Base64 back to a string
const decoded = Buffer.from("SGVsbG8sIFdvcmxkIQ==", "base64").toString("utf8")
// → "Hello, World!"

// Encode a file to Base64
const fs = require("fs")
const fileData = fs.readFileSync("image.png")
const base64Image = fileData.toString("base64")
const dataUri = `data:image/png;base64,${base64Image}`

Base64 in Python

Python's standard library includes the base64 module for encoding and decoding. It supports standard Base64, Base64URL, and other variants:

import base64

# Encode bytes to Base64
encoded = base64.b64encode(b"Hello, World!")
# → b'SGVsbG8sIFdvcmxkIQ=='

# Decode Base64 to bytes
decoded = base64.b64decode("SGVsbG8sIFdvcmxkIQ==")
# → b'Hello, World!'

# URL-safe Base64 (for JWTs and URLs)
url_encoded = base64.urlsafe_b64encode(b"Hello, World!")
# → b'SGVsbG8sIFdvcmxkIQ=='

Encoding Files in the Browser with FileReader

The browser's FileReader API lets you read files selected by the user and encode them as Base64 data URIs. This is useful for image preview before upload, or for sending file data in a JSON API request:

function encodeFileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => resolve(reader.result)
    reader.onerror = reject
    reader.readAsDataURL(file) // Returns "data:image/png;base64,..."
  })
}

// Usage with a file input
const input = document.querySelector('input[type="file"]')
input.addEventListener("change", async (e) => {
  const file = e.target.files[0]
  const base64 = await encodeFileToBase64(file)
  console.log(base64) // data:image/png;base64,iVBORw0KGgo...
})

For modern browsers, the btoa() and atob() functions provide simple Base64 encoding and decoding for strings. However, they only handle ASCII strings — for Unicode text, you need to encode to UTF-8 bytes first using TextEncoder before passing to btoa().

Encode and Decode Base64 Online

Use these free tools to encode or decode Base64 instantly in your browser:

Base64 Performance Considerations

Base64 encoding is convenient, but it comes with performance trade-offs that every developer should understand. Using Base64 in the wrong context can bloat your page size, increase load times, and defeat browser caching. Knowing when Base64 helps and when it hurts is the difference between optimized and wasteful implementations.

The 33% Size Increase

Base64 encoding increases data size by approximately 33%. This happens because every 3 bytes of binary data become 4 bytes of ASCII text. A 100KB image becomes approximately 133KB when Base64 encoded. For small assets this overhead is negligible, but for larger files it adds up quickly and directly impacts bandwidth and page weight.

// Size comparison
Original file:     100 KB
Base64 encoded:    ~133 KB (+33%)
Gzipped Base64:    ~101 KB (compression helps, but still larger)

// The math: 3 bytes → 4 characters
// Overhead = (4/3 - 1) = 33.3%

// Real-world impact on a page with 10 small icons:
// 10 × 2KB icons as separate files = 20KB (cacheable)
// 10 × 2KB icons as Base64 inline  = 26.6KB (not cacheable separately)

The 33% overhead is compounded when the Base64 string is embedded in HTML or CSS — the encoded data cannot benefit from image-specific compression algorithms. However, when the entire page is gzipped during transfer, the effective overhead is reduced because gzip compresses Base64 text reasonably well.

Base64 Inline Images vs Separate File Requests

The traditional argument for Base64 inline images was eliminating HTTP requests. Each separate image file required a new TCP connection, DNS lookup, and round-trip. However, HTTP/2 and HTTP/3 have largely solved this problem with multiplexing — multiple files can be transferred over a single connection simultaneously.

ApproachProsCons
Base64 inlineNo extra HTTP request, single file deployment33% larger, not cacheable separately, blocks rendering
Separate fileCacheable, smaller transfer, lazy-loadableExtra HTTP request (minimal with HTTP/2)

When Base64 Hurts Performance

Base64 is actively harmful to performance in several scenarios:

  • Large files (over 10KB): The 33% overhead becomes significant. A 500KB image becomes 665KB of inline text that cannot be cached independently.
  • Uncacheable inline data: When Base64 is embedded in HTML, it must be re-downloaded on every page load. Separate image files get cached by the browser and served instantly on subsequent visits.
  • CSS bundle bloat: Base64 images in CSS increase your stylesheet size. The entire CSS file must be downloaded and parsed before any styles apply — large data URIs delay the critical rendering path.
  • Repeated assets: If the same icon appears on multiple pages, Base64 duplicates the data in each page's HTML. A separate file is cached once and reused everywhere.

Alternatives: Blob URLs and Object URLs

For client-side file previews (like showing an image before upload), Blob URLs are often a better choice than Base64. They are created instantly without encoding overhead, use no extra memory for the encoded representation, and can be revoked when no longer needed.

// ❌ Base64 approach — slower, uses more memory
const reader = new FileReader()
reader.onload = (e) => {
  img.src = e.target.result // data:image/png;base64,... (33% larger in memory)
}
reader.readAsDataURL(file)

// ✅ Blob URL approach — instant, no encoding overhead
const blobUrl = URL.createObjectURL(file)
img.src = blobUrl // blob:http://localhost/abc-123 (zero encoding cost)

// Clean up when done to free memory
URL.revokeObjectURL(blobUrl)

// When to use each:
// - Blob URL: client-side previews, temporary display
// - Base64: when you need to serialize the data (send in JSON, store in DB)

The Sweet Spot: Files Under 5KB

The general consensus among web performance experts is that files under 5KB benefit from Base64 inlining. At this size, the 33% overhead adds only ~1.6KB, which is less than the overhead of a separate HTTP request (even with HTTP/2, there is connection framing, headers, and scheduling overhead). Build tools like Webpack and Vite use this threshold by default in their asset inlining rules.

// Vite config — inline assets under 4KB as Base64
export default defineConfig({
  build: {
    assetsInlineLimit: 4096 // 4KB threshold (default)
  }
})

// Webpack config — inline small assets
module.exports = {
  module: {
    rules: [{
      test: /\.(png|jpg|gif|svg)$/,
      type: "asset",
      parser: {
        dataUrlCondition: {
          maxSize: 5 * 1024 // 5KB threshold
        }
      }
    }]
  }
}

// Rule of thumb:
// < 5KB  → Base64 inline (saves a request)
// > 5KB  → Separate file (cacheable, smaller transfer)

Base64 in Every Language (Quick Reference)

Every language handles Base64 slightly differently. Here's the reference I keep coming back to when switching between projects:

// JavaScript (Browser)
btoa("Hello")                          // → "SGVsbG8="
atob("SGVsbG8=")                       // → "Hello"
// ⚠️ btoa/atob only handle ASCII! For Unicode:
btoa(unescape(encodeURIComponent("café")))  // Encode Unicode
decodeURIComponent(escape(atob(encoded)))   // Decode Unicode

// JavaScript (Node.js)
Buffer.from("Hello").toString("base64")           // → "SGVsbG8="
Buffer.from("SGVsbG8=", "base64").toString()      // → "Hello"
// Node.js handles Unicode automatically

// Python
import base64
base64.b64encode(b"Hello").decode()    # → "SGVsbG8="
base64.b64decode("SGVsbG8=").decode()  # → "Hello"
# URL-safe variant:
base64.urlsafe_b64encode(b"data")      # Uses - and _ instead of + and /

// Java
import java.util.Base64;
Base64.getEncoder().encodeToString("Hello".getBytes());  // → "SGVsbG8="
new String(Base64.getDecoder().decode("SGVsbG8="));      // → "Hello"
// URL-safe:
Base64.getUrlEncoder().encodeToString(data);

// Go
import "encoding/base64"
encoded := base64.StdEncoding.EncodeToString([]byte("Hello"))  // → "SGVsbG8="
decoded, _ := base64.StdEncoding.DecodeString("SGVsbG8=")      // → "Hello"

// PHP
base64_encode("Hello")    // → "SGVsbG8="
base64_decode("SGVsbG8=") // → "Hello"

// C# (.NET)
Convert.ToBase64String(Encoding.UTF8.GetBytes("Hello"))  // → "SGVsbG8="
Encoding.UTF8.GetString(Convert.FromBase64String("SGVsbG8="))  // → "Hello"

// Command Line
echo -n "Hello" | base64          # Encode (Linux/macOS)
echo "SGVsbG8=" | base64 -d       # Decode (Linux)
echo "SGVsbG8=" | base64 -D       # Decode (macOS)

Common Base64 Mistakes (I've Made All of These)

⚠️ Treating Base64 as encryption

Impact: Your 'hidden' API keys in client-side code are trivially decoded by anyone. Base64 provides zero security.

Fix: Never use Base64 to 'hide' secrets. If data needs protection, use actual encryption (AES-256-GCM) or keep it server-side.

⚠️ Double-encoding data

Impact: Encoding already-encoded data produces garbage like 'U0dWc2JHOD0=' (Base64 of 'SGVsbG8='). Decoding once gives you the intermediate encoding, not the original.

Fix: Track encoding state. If you receive Base64 from an API, don't encode it again. Use naming conventions: rawBytes vs encodedString.

⚠️ Using btoa() with Unicode in the browser

Impact: btoa('café') throws 'InvalidCharacterError'. It only handles Latin1 characters (code points 0-255).

Fix: Convert to UTF-8 bytes first: btoa(unescape(encodeURIComponent(str))). Or use Buffer in Node.js which handles UTF-8 natively.

⚠️ Not using URL-safe Base64 for URLs

Impact: Standard Base64 uses + and / which have special meaning in URLs. Your data gets corrupted when passed as URL parameters.

Fix: Use Base64URL encoding: replace + with -, / with _, and remove trailing = padding. Most languages have a URL-safe variant.

⚠️ Base64-encoding large files in JSON APIs

Impact: A 10MB file becomes ~13.3MB after Base64 encoding. This bloats payloads, uses more bandwidth, and increases parse time.

Fix: For files >5KB, use multipart/form-data uploads or pre-signed URLs. Reserve Base64 for small payloads (thumbnails, avatars, config).

⚠️ Including newlines in Base64 strings

Impact: Some Base64 encoders insert line breaks every 76 characters (RFC 2045 MIME format). Decoders that don't expect them choke.

Fix: Strip newlines before decoding, or use encoders that produce single-line output (most modern ones do by default).

The Bottom Line

Base64 is a tool, not a solution. It solves one problem really well: getting binary data through text-only systems without corruption. That's it. It's not security, it's not compression, and it makes things bigger, not smaller.

Use it for: embedding tiny images, sending files in JSON APIs, email attachments, data URIs. Don't use it for: hiding secrets, large files, or anything where you think "encoding" means "protecting." Once that clicks, you'll use Base64 with confidence — and stop Googling "is Base64 secure" at 1 AM.