Comparison
Base64 vs base64url: two characters that matter
Same alphabet, different last two characters, different padding. One-line difference, totally different use cases.
Both are defined in RFC 4648. Standard Base64(§4) and base64url(§5) differ in exactly three details: two alphabet substitutions and the treatment of padding. That’s it. But mixing them up breaks JWTs, signed cookies, and anything else that travels in a URL.
The diff
| Property | Standard Base64 | Base64url |
|---|---|---|
| 62nd character | + | - |
| 63rd character | / | _ |
| Padding | = to multiple of 4 | Typically omitted |
| URL-safe? | No (+, /, = need percent-encoding) | Yes |
| Defined in | RFC 4648 §4 | RFC 4648 §5 |
The same input, both encodings
Encoding https://example.com/?q=hello world:
- Standard:
aHR0cHM6Ly9leGFtcGxlLmNvbS8/cT1oZWxsbyB3b3JsZA== - Base64url:
aHR0cHM6Ly9leGFtcGxlLmNvbS8_cT1oZWxsbyB3b3JsZA
Note the / in the standard form becomes_ in base64url, and the trailing ==is dropped.
Why URLs need their own variant
The standard Base64 alphabet includes + and/. Both are reserved in URL syntax:
/is the path separator.+is, by convention in form-encoded query strings, interpreted as a space.=is the key-value delimiter in query strings.
Embedding a standard-Base64 string in a URL requires percent-encoding all three: + becomes%2B, / becomes %2F,= becomes %3D. The string gets longer and harder to read. base64url avoids the percent encoding by picking characters that are already URL-safe.
Why padding is optional in base64url
Standard Base64 always pads the output to a multiple of 4 characters using =. This isn’t mathematically necessary — the decoder can infer the original length from the output length modulo 4. Padding exists for streaming decoders that need to know the boundary between concatenated chunks.
Base64url skips padding because: (a) it’s redundant, (b) = needs percent-encoding in URLs, and (c) trailing equals signs in URLs sometimes confuse path parsers, CDNs, and cache keys.
Where each is used
| Use case | Variant |
|---|---|
| Email (MIME) attachments | Standard Base64 |
HTTP Basic Auth (Authorization: Basic …) | Standard Base64 |
Data URIs (data:image/png;base64,…) | Standard Base64 |
| JWT (all three segments) | Base64url, no padding |
| OAuth 2.0 PKCE code verifier/challenge | Base64url, no padding |
| WebPush message keys, JOSE/JWS/JWE | Base64url, no padding |
| Signed cookies (Rails, Django, etc.) | Base64url, no padding |
The decoding gotcha
Many libraries’ default Base64 decoder fails on base64url input. Python’s base64.b64decoderejects - and _; Node.js’s Buffer.from(s, "base64")is permissive but the inverse (.toString("base64")) outputs standard Base64.
Cross-language hacks: replace before decoding.
// base64url → standard Base64
const standard = base64url
.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(Math.ceil(base64url.length / 4) * 4, "=");Better: use a library that explicitly supports base64url (Node 16+ has Buffer.from(s, "base64url"); Python has base64.urlsafe_b64decode).
The pragmatic rule
If the encoded value will appear in a URL, in a JWT, in a signed cookie, or as a path segment — use base64url. If it’s for an email attachment, HTTP header (other than cookies), or a JSON data URI — use standard Base64.
Our Base64 encoder handles both variants, with a toggle for the alphabet. The JWT decoder assumes base64url because the standard requires it.
Related
Published May 16, 2026