URL-Safe Base64 vs Standard Base64: A Practical Guide
2026-09-17 · 995 words
Standard Base64 uses 64 characters: A–Z, a–z, 0–9, and then + and /. Those last two are the problem. In a URL, + means space in application/x-www-form-urlencoded; / is a path separator. In a filename, / is a directory separator. So a string that is perfectly valid Base64 becomes structurally dangerous the moment you put it in a query parameter, a cookie value, or a JWT.
The fix is an alphabet substitution, and it is where a surprising number of decoding failures begin.
The substitution, and the name it goes by
URL-safe Base64 — also called base64url, or RFC 4648 §5 — replaces the two troublesome characters:
+becomes-(hyphen)/becomes_(underscore)
Everything else is identical. Sixty-two characters are shared, so most strings are valid in both alphabets and decode to the same bytes. Only strings containing +, /, - or _ tell you which alphabet the producer had in mind.
Two of those four characters are ambiguous, which is the heart of the problem. If a string contains -, it is nearly certainly base64url. If it contains +, it is standard. But a string containing only [A-Za-z0-9] and = is valid in both, and there is no way to tell from the text alone. Decoding it either way gives the same answer, so ambiguity is harmless there.
The dangerous middle case is a standard Base64 string that happens to contain - or _... except it cannot, because those characters are not in the standard alphabet. Which means the rule is simpler than it looks: if you see - or _, treat it as base64url and convert; if you see + or /, treat it as standard. If you see a mix, the string is not valid in either alphabet and something upstream concatenated two different strings.
Padding, again
RFC 4648 §5 allows base64url to omit = padding, and most producers of JWTs and URL tokens do exactly that. The padding carries no information — it is determined by the length — so it is pure overhead in a token that will be embedded in a header.
This is why a raw JWT segment often has a length that is not a multiple of four:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
That is 36 characters, divisible by 4 by luck. A payload segment is often not. Strict decoders will reject it, so restore padding before decoding — it is deterministic and lossless:
const padded = s + '='.repeat((4 - (s.length % 4)) % 4);
Converting between the two
In JavaScript, browsers, and Node, the conversion is a two-character substitution in each direction, applied to the text form:
const toUrlSafe = (b64) => b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const fromUrlSafe = (s) => {
const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
return b64 + '='.repeat((4 - (b64.length % 4)) % 4);
};
Node has it built in, in both directions, on both buffers and strings:
Buffer.from(bytes).toString('base64url'); // encode
Buffer.from(token, 'base64url').toString('utf8'); // decode
Python calls it urlsafe_b64decode, and its counterpart is urlsafe_b64encode:
import base64
raw = base64.urlsafe_b64decode(token + '=' * (-len(token) % 4))
Note the asymmetry that catches people out: urlsafe_b64decode accepts both alphabets' characters in some versions, so it will not complain about a standard string containing +. It is forgiving in the direction where forgiveness is harmless, and strict about padding in the direction where being wrong matters.
Why a decoder should report the alphabet it used
If a string contains - or _ and you decode it as standard Base64, you get an error at the exact character that is out of range — which at least tells you something is wrong. But some decoders quietly translate the characters for you, and that is where the worst failure mode lives: a string that is actually standard Base64 containing a legitimate + or / gets mangled into the wrong bytes because the decoder assumed base64url.
For a picture, that means an image that decodes without error and will not render. For a JWT segment, it means a signature that fails to verify with no explanation.
That is why the decoder reports which alphabet it chose, and why a checkbox is offered to override it. Our URL-safe Base64 to Image page exists for the case where you know the source used the URL-safe alphabet and want it treated that way from the start, without guessing. When the input contains - or _ and the switch is off, the tool says so explicitly rather than silently picking one interpretation.
The practical rule
Three questions, in order:
- Will this string travel through a URL, a filename, or a cookie? If yes, produce base64url. It costs nothing and removes an entire class of escaping bugs.
- Does the string contain
+or/? Treat it as standard Base64. Those characters cannot appear in base64url. - Does it contain
-or_? Treat it as base64url.
And one habit worth building: never re-encode a string you received before decoding it. Converting a standard string to URL-safe form and back is lossless, but doing it speculatively — normalising before you know the alphabet — is how a + becomes a space in a query string and then becomes a different byte after decoding. Decode first, from the form you received, then re-encode in whatever form the next consumer needs.
Where this shows up in practice
JWTs are the most familiar case: three base64url segments separated by dots, no padding, no line breaks. Their compactness is the whole point, and standard Base64 would break every one of them at the + and /.
Less obvious cases include opaque session cookies, signed URLs (AWS presigned URLs use standard Base64 in some parameters and URL-safe in others), correlation IDs in headers, and image payloads passed as query parameters in APIs that were never designed to carry binary data. In all of them, the failure looks the same: a string that "should decode" produces either an error at one character position, or bytes that decode cleanly and mean nothing.
The second outcome is the one to watch for. If you decoded something and the result is not the file or the payload you expected, check the alphabet before you check anything else — see Base64 image not showing for the full order of checks, which starts with exactly that question. If you would rather have the alphabet handled for you, the URL-safe Base64 to Image page treats - and _ as the expected alphabet from the start, and Base64 to Image auto-detects it while telling you which interpretation it used.