← All writing

FIELD NOTES

Understanding Base64 Encoding: Why We Use It, How It Works, and Trade-offs

In this note

Whenever we transfer raw binary data across text-first protocols or formats, Base64 almost always makes an appearance. From embedding images in HTML to storing cryptographic hashes and passing binary payloads in JSON APIs, Base64 is an everyday backend staple.

Yet two questions commonly arise: Why do we need it? and How does it actually convert bits under the hood?


Why Do We Need Base64?

Many formats and interfaces are text-oriented. JSON strings, for example, represent text rather than arbitrary byte sequences, and some transport contexts impose character restrictions.

When an interface expects text, raw bytes need an agreed representation. Arbitrary binary values may contain byte sequences that are invalid or meaningful to that interface.

  • Binary data can contain non-printable control characters (like 0x00 null bytes, EOF markers, or carriage returns).
  • Intermediate gateways, parsers, and proxies can strip, normalize, or misinterpret those raw bytes, corrupting the payload.

Base64 solves this by taking arbitrary binary data and translating it into a restricted set of 64 safe, printable ASCII characters that can be represented in text-oriented formats. The standard Base64 alphabet is not automatically safe in every URL or filename context; use the URL-safe variant where required.

Base64 is encoding, not encryption. It provides no confidentiality. Anyone with the encoded value can decode it without a secret key.


Real-World Use Cases

  1. JSON Payloads: JSON supports strings, numbers, booleans, null, arrays and objects, but it has no native byte-string type. You cannot stick raw binary bytes into a JSON attribute. Converting an image or file buffer to a Base64 string allows it to be embedded directly into standard JSON fields.

  2. Inline Assets (Data URLs in Browsers): Small images, SVGs, or fonts can be rendered directly in HTML/CSS without an extra HTTP round-trip:

    1
    
    <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA..." alt="Icon" />
    
  3. Cryptography (PBKDF2, Argon2id, JWTs): Password hashing algorithms and cryptographic keys produce raw byte arrays (like binary salts and hashes). Storing raw bytes in database text columns is error-prone. Base64 (and its URL-safe variant Base64URL) gives these cryptographic outputs a clean text representation.

  4. Email Attachments (MIME): SMTP originally supported only 7-bit ASCII text. MIME relies on Base64 encoding to send PDFs, photos, and ZIP files across email servers without byte truncation.


How Base64 Works: The 24-Bit / 4-Chunk Math

The name Base64 comes from its alphabet of 64 characters (2⁶ = 64):

  • Uppercase letters: A–Z (26)
  • Lowercase letters: a–z (26)
  • Digits: 0–9 (10)
  • Special symbols: + and / (2)

The = character is padding used when the input length is not divisible by three; it is not part of the 64-character alphabet.

Because each character represents 64 possibilities, each character encodes exactly 6 bits of information.

The encoding process groups 3 bytes (24 bits) and slices them into 4 chunks of 6 bits:

1
2
3
4
5
6
7
Input:      [ Byte 1 (8 bits) ]  [ Byte 2 (8 bits) ]  [ Byte 3 (8 bits) ]
                 \                    |                    /
Total:                        24 Bits
                 /           /                 \           \
Regrouped:  [ 6 bits ]    [ 6 bits ]        [ 6 bits ]    [ 6 bits ]
               │             │                 │             │
Output:     Char 1        Char 2            Char 3        Char 4

Step-by-Step Example: Encoding the String "Man"

  1. ASCII input: 'M', 'a', 'n'
  2. Binary bytes (8 bits each):
    • 'M' = 01001101
    • 'a' = 01100001
    • 'n' = 01101110
    • Combined stream: 010011010110000101101110 (24 bits)
  3. Split into four 6-bit groups:
    • 010011 → Decimal 19 → Index 19 is T
    • 010110 → Decimal 22 → Index 22 is W
    • 000101 → Decimal 5 → Index 5 is F
    • 101110 → Decimal 46 → Index 46 is u
  4. Result: "TWFu"

What About Padding (=)?

What happens if your input data isn’t a multiple of 3 bytes? Base64 uses the = character to signal padding:

  • 1 byte left over (8 bits): Requires 4 zero-bits of padding to make two 6-bit chunks → encoded as 2 characters + ==.
  • 2 bytes left over (16 bits): Requires 2 zero-bits of padding to make three 6-bit chunks → encoded as 3 characters + =.

The Trade-off: The 33% Size Increase

Because 3 bytes of input turn into 4 bytes of characters:

(4 - 3) / 3 = 1 / 3 ≈ 33.3%

For large inputs, the encoded representation is approximately one-third larger than the raw bytes, before any transport-specific line wrapping or container overhead. For small tokens or hashes, this overhead is negligible; for large video or file streams, transferring raw binary or multipart forms is significantly more efficient.


Java Code Example

Java provides built-in, highly optimized Base64 support via java.util.Base64:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Base64Example {
    public static void main(String[] args) {
        String originalText = "Man";

        // Encoding
        byte[] inputBytes = originalText.getBytes(StandardCharsets.UTF_8);
        String encoded = Base64.getEncoder().encodeToString(inputBytes);

        System.out.println("Input:   " + originalText + " (" + inputBytes.length + " bytes)");
        System.out.println("Encoded: " + encoded + " (" + encoded.length() + " chars)");
        // Encoded: TWFu

        // Decoding
        byte[] decodedBytes = Base64.getDecoder().decode(encoded);
        String decodedText = new String(decodedBytes, StandardCharsets.UTF_8);

        System.out.println("Decoded: " + decodedText);
    }
}