Base64 vs Base64url
On this page
TL;DR. Standard base64 (
+,/, padding required) is the historical default. Base64url (-,_, padding optional) replaces the two URL-unsafe characters so the output can sit in a URL or filename without escaping. Both encode the same bytes — only the surface alphabet differs.
At a glance
| Standard base64 | Base64url (RFC 4648 §5) | |
|---|---|---|
| Position 62 | + | - |
| Position 63 | / | _ |
| Padding | Required (=) | Optional |
| URL-safe out of the box | No | Yes |
| MIME / email | Standard | No |
| JWT | No | Standard |
| Filenames | Risky | Safe |
| Spec | RFC 4648 §4 | RFC 4648 §5 |
What changes
The 64-character alphabet has three differences:
Standard: ABC...XYZabc...xyz0123456789+/
Base64url: ABC...XYZabc...xyz0123456789-_
That’s it. Positions 62 and 63 swap from URL-reserved characters to URL-safe ones. Everything else (positions 0–61) is identical.
The padding rule changes too: standard base64 requires = to fill
the output to a multiple of 4 characters; base64url makes it optional.
Why two variants exist
When base64 was specified for email (RFC 989, 1987), the alphabet was chosen to maximize compatibility with the 7-bit ASCII-only mail infrastructure of the time. URLs weren’t a consideration.
When the web came along, putting base64 in URL parameters became
common — but + and / are URL-reserved, so the encoded value had to
be URL-encoded again (+ → %2B, / → %2F). That’s wasteful and
error-prone, especially for tokens that pass through multiple systems.
RFC 4648 (2006) standardized base64url to fix this without breaking the existing standard.
Where each is the right choice
Use standard base64 when:
- Email attachments / MIME bodies. Standards explicitly require the standard alphabet.
- Decoding values from systems that emit standard form (most CLI tools, most language stdlib defaults).
- HTTP headers. Most header values aren’t URL-encoded, so
+and/don’t conflict. - Configuration files (YAML, JSON, .env). No URL escaping happens.
- Database BLOB-as-string columns. Same reasoning.
Use base64url when:
- JWT. All three parts of every JWT are base64url, padding stripped. Required by spec.
- URL query parameters.
?token=<base64url>works directly;?token=<base64>needs escaping. - URL path segments.
/share/<base64url>is fine;/share/<base64>would have/mid-path. - Cookie values. Either works, but base64url avoids any escaping concerns.
- Filenames. Most filesystems accept
-and_but reject/.+is allowed but visually unusual. - OAuth state and PKCE codes. RFC 7636 specifies base64url.
- Any custom token / identifier you’ll embed in URLs. Avoids the URL-encoding step.
Converting between them
A two-line conversion in any language:
// Standard → URL-safe
const urlSafe = standard.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
// URL-safe → Standard
const padded = urlSafe + "=".repeat((4 - urlSafe.length % 4) % 4);
const standard = padded.replace(/-/g, "+").replace(/_/g, "/");
The bytes encoded are identical; only the surface alphabet differs.
Padding: keep it or strip it?
The classic gotcha. Three rules:
- Standard base64 — keep padding. Many decoders reject unpadded input.
- Base64url for JWT — strip padding. The spec requires it.
- Base64url for everything else — your choice; the decoder accepts both.
If you’re consuming someone else’s base64url output and the decoder
fails with “invalid length,” try adding = until the length is a
multiple of 4.
Same input, both outputs
Original bytes: [0xff, 0xfb, 0xff, 0xfb] (4 bytes that exercise + and /)
Standard: "//v/+w=="
Base64url: "__v_-w" (no padding)
Base64url: "__v_-w==" (padding kept)
Notice how the standard output uses both + and /, while the URL-safe
version uses _ and -. Same data, different alphabet.
Languages that distinguish them
Most modern standard libraries provide both:
// Node.js (16+)
Buffer.from(text).toString("base64");
Buffer.from(text).toString("base64url");
// Python
import base64
base64.b64encode(data) // standard
base64.urlsafe_b64encode(data) // URL-safe
// Go
import "encoding/base64"
base64.StdEncoding.EncodeToString(data) // standard
base64.URLEncoding.EncodeToString(data) // URL-safe with padding
base64.RawURLEncoding.EncodeToString(data) // URL-safe without padding
// Rust (base64 crate)
use base64::{Engine, engine::general_purpose};
general_purpose::STANDARD.encode(data)
general_purpose::URL_SAFE_NO_PAD.encode(data)
When the library has both, pick the right one explicitly. Mixing silently leads to the “works in dev, breaks in prod” pattern.
Common mistakes
- Encoding standard, decoding URL-safe (or vice versa) without normalizing. Roundtrips silently fail because the byte values at positions 62 and 63 don’t match.
- URL-encoding base64url anyway. Defeats the purpose; the result will have
%2D(URL-encoded-) and%5F(URL-encoded_), which other systems may misinterpret. - Decoding base64url with a strict standard decoder. Fails on
-or_. Normalize first. - Stripping padding when the consumer requires it. Always test both directions of your encode/decode pair before deploying.
Tools on this site
- Standard encoder/decoder — toggle URL-safe with the checkbox
- Base64url focused — URL-safe by default
- What is Base64? — fundamentals if you need them