Three Encodings, Three Different Jobs
URL encoding, Base64, and hex are all ways to represent data as printable ASCII text — but they solve completely different problems. Using the wrong one is a common source of bugs:
- Passing a Base64 string directly in a URL breaks when it contains
+and/ - URL-encoding binary data instead of Base64ing it produces strings 3× the original size
- Storing a password hash as Base64 instead of hex is fine, but mixing them in a system causes comparison failures
Here's when to reach for each one.
URL Encoding (Percent Encoding)
Purpose: Make arbitrary text safe to include in a URL.
How it works: Every character that isn't a URL-safe alphanumeric or one of - _ . ~ gets replaced with % followed by its two-digit hex code.
Space → %20
& → %26
= → %3D
/ → %2F
+ → %2B
"hello world" → "hello%20world"
When to use it:
- Query string parameters:
?name=John+Doeor?name=John%20Doe - Path segments containing special characters
- Form data submission (
application/x-www-form-urlencoded)
When NOT to use it:
- Binary data (images, files) — the output is enormous
- Data that will be stored in a database — decode before storing
- Passwords or secrets — they should never appear in URLs at all
// JavaScript
encodeURIComponent('hello world & more') // "hello%20world%20%26%20more"
decodeURIComponent('hello%20world') // "hello world"
// encodeURI vs encodeURIComponent
encodeURI('https://example.com/path?q=hello world')
// "https://example.com/path?q=hello%20world" ← leaves : // ? = & intact
encodeURIComponent('hello world')
// "hello%20world" ← encodes everything including : / ? = &
from urllib.parse import quote, unquote, urlencode
quote('hello world & more') # 'hello%20world%20%26%20more'
quote('hello/world', safe='/') # 'hello/world' — / not encoded
unquote('hello%20world') # 'hello world'
# For query strings
urlencode({'name': 'John Doe', 'age': 30})
# 'name=John+Doe&age=30' ← spaces as + in query strings
Base64
Purpose: Represent binary data as printable ASCII text.
How it works: Takes 3 bytes of binary data and encodes them as 4 ASCII characters from a 64-character alphabet (A-Z a-z 0-9 + /). Output size is always 4/3 (33%) larger than the input.
"Hello" → "SGVsbG8="
When to use it:
- Embedding binary files (images, PDFs) in JSON or XML
- Email attachments (MIME encoding)
- HTTP Basic Authentication headers (
Authorization: Basic dXNlcjpwYXNz) - Storing small binary blobs in text fields
- Data URLs (
<img src="data:image/png;base64,iVBOR...">)
When NOT to use it:
- URLs — the
+and/characters are valid Base64 but have special meaning in URLs. Use Base64url instead (replaces+with-and/with_, removes padding) - Large files — the 33% size overhead adds up
- Data that needs to be human-readable
// Browser
btoa('Hello World') // "SGVsbG8gV29ybGQ="
atob('SGVsbG8gV29ybGQ=') // "Hello World"
// Node.js
Buffer.from('Hello World').toString('base64') // "SGVsbG8gV29ybGQ="
Buffer.from('SGVsbG8gV29ybGQ=', 'base64').toString() // "Hello World"
// Base64url (URL-safe, no padding)
Buffer.from('Hello World').toString('base64url') // "SGVsbG8gV29ybGQ"
import base64
base64.b64encode(b'Hello World') # b'SGVsbG8gV29ybGQ='
base64.b64decode('SGVsbG8gV29ybGQ=') # b'Hello World'
# URL-safe Base64
base64.urlsafe_b64encode(b'Hello World') # b'SGVsbG8gV29ybGQ='
Hexadecimal (Hex) Encoding
Purpose: Represent binary data as a human-readable string of hex digits.
How it works: Each byte becomes exactly two hex digits (0–9, a–f). A 32-byte SHA-256 hash becomes a 64-character hex string.
0x48 0x65 0x6c 0x6c 0x6f → "48656c6c6f"
When to use it:
- Cryptographic hashes (SHA-256, MD5):
a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3 - Binary identifiers that need to be readable: transaction IDs, fingerprints
- Debugging binary data
- Color codes:
#FF5733 - MAC addresses:
00:1A:2B:3C:4D:5E
When NOT to use it:
- When space efficiency matters — hex is 2× the size of the original binary (compared to Base64's 1.33×)
- Embedding binary in JSON or HTTP — use Base64 instead
// Node.js
crypto.createHash('sha256').update('hello').digest('hex')
// "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
Buffer.from('Hello').toString('hex') // "48656c6c6f"
Buffer.from('48656c6c6f', 'hex').toString() // "Hello"
import hashlib
hashlib.sha256(b'hello').hexdigest()
# "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
b'Hello'.hex() # '48656c6c6f'
bytes.fromhex('48656c6c6f') # b'Hello'
Side-by-Side Comparison
| Property | URL Encoding | Base64 | Hex |
|---|---|---|---|
| Input | Text strings | Any binary | Any binary |
| Output size | Variable (up to 3×) | 4/3 × input | 2× input |
| URL-safe | Yes (that's the point) | No (use Base64url) | Yes |
| Human-readable | Somewhat | No | Somewhat |
| Standard use | Query params, form data | Binary in text contexts | Hashes, fingerprints |
| Padding | None | = padding |
None |
The Base64 in URLs Trap
JWT tokens use Base64url — not standard Base64. If you decode a JWT's header or payload with regular Base64, it works because the specific bytes happen to not contain + or /. But if your JWT library produces standard Base64 and you put it in a URL without re-encoding, any + becomes a space and / creates a path segment.
// Safe JWT in URL (base64url, no padding)
const header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'; // no + / or =
// Dangerous: standard base64 in URL
const standard = Buffer.from(data).toString('base64');
// May contain + / = — must re-encode for URLs:
const urlSafe = standard.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
→ Encode and decode URLs with the URL Encoder or encode Base64 with the Base64 Converter.