Base64-encode a value, drop it into a URL query parameter, and roughly one time in three you'll get a broken link — because standard base64's alphabet includes +, /, and =, and all three collide with characters that already mean something in a URL. Base64url exists specifically to fix this, and the fix is small enough that reaching for the wrong variant is a common, avoidable bug.
What's Actually Different
Standard base64 (defined in RFC 4648, section 4) uses the alphabet A-Z, a-z, 0-9, +, /, with = as padding to make the output length a multiple of 4. Base64url (RFC 4648, section 5) is the same scheme with two substitutions and one omission:
| | Standard base64 | Base64url |
| --- | --- | --- |
| Character at index 62 | + | - |
| Character at index 63 | / | _ |
| Padding | = (required) | Typically omitted |
| Safe in URL path/query | No | Yes |
| Safe in filenames | No (/ is a path separator) | Yes |
| Safe in HTTP headers unescaped | Mostly, but + can be misinterpreted as a space in some contexts | Yes |
The underlying encoding algorithm — mapping 3 bytes of input to 4 output characters — is identical. Only the last two alphabet symbols and the padding convention change.
Why + and / Specifically Break URLs
In a URL query string, + is a legacy convention for encoding a space (from application/x-www-form-urlencoded), so a base64 string containing + can get silently decoded as a space by form-parsing middleware before it ever reaches your base64 decoder. / is the path segment separator — put a raw base64 string with a / into a URL path segment and you've just added an extra, unintended path boundary. Both are "sometimes works, sometimes doesn't" bugs depending on which layer of your stack touches the string first, which makes them nasty to track down.
The = padding is a smaller problem — it's technically URL-safe on its own — but it's often dropped anyway in base64url implementations because trailing = characters look like key-value delimiters to some naive query-string parsers, and because it's derivable: given the encoded string's length, you always know how much padding it should have had.
Converting Between the Two in JavaScript
Node and browsers don't have a single built-in "encode as base64url" call in every context, so the conversion is usually a small string transform layered on top of standard base64:
function toBase64Url(base64) {
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
function fromBase64Url(base64url) {
let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
// Restore padding to a multiple of 4
while (base64.length % 4) {
base64 += '=';
}
return base64;
}
const encoded = toBase64Url(Buffer.from('hello world?').toString('base64'));
// 'aGVsbG8gd29ybGQ_'
const decoded = Buffer.from(fromBase64Url(encoded), 'base64').toString('utf8');
// 'hello world?'
Node's Buffer actually supports 'base64url' as a native encoding directly, which skips the manual transform entirely:
const encoded = Buffer.from('hello world?').toString('base64url');
// 'aGVsbG8gd29ybGQ_'
const decoded = Buffer.from(encoded, 'base64url').toString('utf8');
// 'hello world?'
In the browser, where there's no Buffer, you're working with btoa/atob (which only handle Latin1 strings, so wrap with TextEncoder/TextDecoder for arbitrary UTF-8) and applying the same character substitution manually.
Where This Comes Up in Practice
JWTs. RFC 7519 specifies that JSON Web Tokens use base64url without padding for all three segments — header, payload, signature. This is precisely because a JWT commonly travels in an Authorization header or a URL query parameter, and standard base64's +///= would need extra escaping in both contexts. If you're hand-rolling JWT encoding for any reason, using standard base64 by mistake produces a token that some strict parsers will reject outright.
Signed URLs and short-lived tokens. Password reset links, magic-link auth, and pre-signed download URLs frequently embed an encoded payload directly in the URL. Base64url avoids the need to percent-encode the token afterward, which keeps the URL shorter and avoids double-encoding bugs where a value gets percent-encoded once by your code and again by an HTTP client library.
Filenames derived from content hashes or IDs. If you're encoding a UUID, hash, or binary identifier into a filename, standard base64's / would be interpreted as a directory separator on most filesystems. Base64url sidesteps that entirely.
Non-cases: if the encoded value only ever lives inside a JSON field, a database column, or an email body — never a URL, path, or filename — plain base64 is fine and there's no reason to switch. The decision point is entirely about where the string travels next, not some general preference for one alphabet over the other.
Quick Reference
Default to base64url whenever the encoded output might end up in a URL, filename, or HTTP header — treat plain base64 as the exception, reserved for contexts you know are purely internal (JSON payloads, database blobs). To encode or decode values interactively while debugging a token or signed link, the Base64 Encoder handles standard base64 conversion — just remember to apply the +// → -/_ substitution afterward if the target context is a URL.
Next time you're building anything that puts an encoded value into a link — password resets, share links, webhook callback URLs — check which base64 variant you're actually emitting before it ships, not after a user reports a broken link with a stray + turned into a space.