Base64 vs Base64URL: What Changes and When to Use Each
Base64URL is standard Base64 with two characters replaced, - for + and _ for /, and usually without the = padding.
Both encodings are defined in RFC 4648: standard Base64 in section 4 and the "URL and filename safe" variant, base64url, in section 5. They split data into 6-bit groups in exactly the same way and share 62 of their 64 characters. Only the characters for the values 62 and 63 differ, plus the convention about padding. That makes converting between them a matter of swapping characters, with no re-encoding of the data, and it makes mixing them up an easy bug that often goes unnoticed.
The two alphabets side by side
| Base64 (RFC 4648, section 4) | Base64URL (RFC 4648, section 5) | |
|---|---|---|
| Values 0 to 61 | A-Z a-z 0-9 | A-Z a-z 0-9 |
| Value 62 | + | - |
| Value 63 | / | _ |
Padding with = | Required, unless the format using it says otherwise | Usually left out |
| Safe in URLs and file names as is | No | Yes |
The same bytes in both forms:
Bytes fb ff bf
Base64 +/+/
Base64URL -_-_
Text <<???>>
Base64 PDw/Pz8+Pg==
Base64URL PDw_Pz8-Pg
Encoded binary data such as hashes, keys and random tokens very often contains a + or / somewhere, so the difference is not a corner case. RFC 4648 itself says base64url should not be regarded as the same encoding as base64, and code should name the variant it expects.
Why standard Base64 breaks in URLs and file names
Each of the three extra characters of standard Base64 has a meaning somewhere else:
+is read as a space when a query string is decoded as form data, which is whatURLSearchParams, PHP's$_GETand most server frameworks do. A token such asab+carrives asab c./separates path segments, so a Base64 value in a path creates extra segments, and some web servers refuse the encoded form%2Fin paths. It is also not allowed in file names on Windows, macOS or Linux.=separates a parameter name from its value.
Percent-encoding the value (%2B, %2F, %3D) works when every piece of code involved does it exactly once. In practice one layer forgets or does it twice, which is why formats designed for URLs use Base64URL and avoid escaping altogether.
Padding rules
Base64 encodes 3 bytes as 4 characters. When the data length is not a multiple of 3, the last group is shorter, and standard Base64 fills it up to 4 characters with =:
| Bytes in the last group | Base64 | Base64URL without padding |
|---|---|---|
1, for example ff | /w== | _w |
2, for example fb ff | +/8= | -_8 |
3, for example fb ff bf | +/+/ | -_-_ |
Section 3.2 of RFC 4648 requires padding unless the specification that refers to it explicitly says otherwise. Section 5 adds that = would normally have to be percent-encoded in a URL, and that padding can be skipped when the length of the data is known anyway. Formats built on Base64URL take that option: JSON Web Signatures (RFC 7515), and therefore JWTs, remove all trailing =, and so do PKCE code challenges (RFC 7636).
Without padding, n bytes produce ⌈4n/3⌉ characters: 16 bytes become 22 characters instead of 24, and 32 bytes become 43 instead of 44. A length that leaves a remainder of 1 when divided by 4 is never valid in either variant.
Where each one is used
| Context | Variant |
|---|---|
| JWT header, payload and signature | Base64URL without padding |
JSON Web Key values such as n and e | Base64URL without padding |
OAuth PKCE code_challenge | Base64URL without padding |
Data URIs, data:image/png;base64,... | Standard Base64 (RFC 2397) |
| Email attachments (MIME) | Standard Base64, lines of at most 76 characters (RFC 2045) |
| PEM certificates and keys | Standard Base64, 64-character lines (RFC 7468) |
| HTTP Basic authentication header | Standard Base64 (RFC 7617) |
| Tokens in URLs, file names, cache keys | Base64URL is the safe choice |
Library defaults follow standard Base64: btoa, Python's b64encode, PHP's base64_encode and Java's Base64.getEncoder() all produce +, / and padding. You have to ask for Base64URL explicitly.
Converting between them
atob and btoa only know standard Base64, so with them you convert the characters and fix the padding yourself:
// Base64 -> Base64URL
const url = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// Base64URL -> Base64
let std = url.replace(/-/g, '+').replace(/_/g, '/');
std += '='.repeat((4 - std.length % 4) % 4);
Recent browsers also have Uint8Array.prototype.toBase64({ alphabet: 'base64url', omitPadding: true }) and Uint8Array.fromBase64(s, { alphabet: 'base64url' }), which work directly on bytes; check support in the browsers you target before relying on them. Other platforms have direct support or a short idiom:
// Node.js
Buffer.from(data).toString('base64url'); // no padding
Buffer.from(s, 'base64url');
# Python
base64.urlsafe_b64encode(data).rstrip(b'=')
base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))
// PHP
rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
base64_decode(strtr($s, '-_', '+/'), true);
// Java
Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
Base64.getUrlDecoder().decode(s);
Python's urlsafe_b64decode rejects input without padding, hence the added = signs. PHP's base64_decode accepts missing padding, even in strict mode.
Pitfalls
- Bugs that pass tests. A standard decoder only fails on Base64URL input that contains
-or_. For random data that is about half of all 16-byte values and about three quarters of 32-byte values, such as SHA-256 digests (1 − (62/64) to the power of 42 ≈ 74%). A test with one or two sample values can easily pass. Why decoders reject these strings, and how differently each language reacts, is covered in Invalid Base64 string. - Lenient decoders hide the mix-up. Node.js
Buffer.from(s, 'base64')accepts both alphabets, which is convenient but means the bug surfaces only in another service. Python's defaultb64decodesilently drops-and_, then returns the wrong bytes or a misleading error about the number of characters. - Base64URL is not URL encoding. Percent-encoding a standard Base64 string gives
%2B,%2Fand%3D; it does not turn it into Base64URL, and a decoder expecting one will not accept the other. - Neither variant hides anything. The payload of a JWT is only Base64URL-encoded JSON, readable by anyone who has the token. Paste one into the JWT Decoder to see its header and payload.
The Base64 Encoder on this site produces standard Base64 with padding; apply the substitution above if you need Base64URL. For decoding, Base64 Decode accepts both alphabets and adds missing padding itself, so you can paste either form.