Base64url vs Base64: URL-Safe Encoding Explained

You pasted a base64 string into a URL and something broke. Or a JWT part refused to decode. Or a filename came out with a slash in the middle of it. The culprit is almost always the same: standard base64 uses two characters that are unsafe in URLs and filenames, and a third character for padding that gets mangled. The fix is a near-identical variant called base64url. They are 98% the same encoding, but the 2% difference is exactly what bites you.

The only real difference: characters 62, 63, and padding

Both variants are defined in RFC 4648. Standard base64 is Section 4; the URL- and filename-safe variant ("base64url") is Section 5. The RFC itself says the two are "technically identical" except for the 62nd and 63rd alphabet characters. Indexes 0 through 61 (A-Z, a-z, 0-9) are byte-for-byte the same. Only the last two positions, plus how padding is handled, differ.

ValueStandard base64base64url
0–25A–ZA–Z
26–51a–za–z
52–610–90–9
62+- (minus)
63/_ (underscore)
Padding= (required by default)= often omitted

Why does this matter? In a URL, + is interpreted as a space in query strings, and / is a path separator. In a filename, / is a directory separator on every major OS. And = is reserved in URIs, so it usually gets percent-encoded to %3D, which bloats and uglifies the string. base64url swaps in - and _ — both safe in URLs and filenames — and typically drops the trailing = entirely.

Why JWTs use base64url (and drop the =)

This is the single most common place developers meet base64url without realizing it. A JWT is three base64url-encoded parts joined by dots: header.payload.signature. The JWS spec (RFC 7515) defines its encoding as "Base64 encoding using the URL- and filename-safe character set defined in Section 5 of RFC 4648, with all trailing '=' characters omitted."

That is why a JWT part will fail in a plain base64 decoder: the decoder sees - or _ as invalid alphabet characters, or chokes on the missing padding. Tokens travel in URLs, Authorization headers, and cookies, so the URL-safe, padding-free form is deliberate. If you are pulling a token apart, the JWT decoder already speaks base64url and splits the three parts for you. For the manual mechanics of doing it by hand, see decode a JWT by hand with base64url.

Convert both ways in JavaScript

You rarely need a library. Standard base64 and base64url are a simple character substitution apart, plus padding. To turn standard base64 into base64url, replace the two characters and strip the padding:

function toBase64url(b64) {
  return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function fromBase64url(b64url) {
  let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
  // restore padding to a multiple of 4
  while (b64.length % 4) b64 += '=';
  return b64;
}

To produce base64url directly from bytes in the browser, encode to standard base64 with btoa first, then convert. Note that btoa only handles Latin-1, so encode UTF-8 text with TextEncoder before calling it (a classic mojibake trap). Decoding back to standard base64 first, then to bytes, is the reverse.

Convert both ways in Python

Python's standard library has this built in. The base64 module provides urlsafe_b64encode and urlsafe_b64decode, which use the -/_ alphabet automatically:

import base64

# bytes -> base64url (Python keeps the '=' padding)
encoded = base64.urlsafe_b64encode(b"hello?world")
b64url = encoded.rstrip(b"=")          # strip padding, JWT-style

# base64url -> bytes (re-add padding first)
pad = b"=" * (-len(b64url) % 4)
decoded = base64.urlsafe_b64decode(b64url + pad)

The key gotcha in both languages is the padding: urlsafe_b64decode and most decoders still expect a length that is a multiple of 4, so you must re-add the = characters you stripped. The math is the same everywhere: pad up to the next multiple of four.

Common gotchas that look like bugs

  • "Invalid character" on decode. You fed base64url (containing - or _) into a standard base64 decoder, or vice versa. Detect which variant you have and use the matching decoder.
  • "Incorrect padding" / "invalid length". The trailing = was stripped (as JWTs do) and your decoder is strict. Re-pad to a multiple of 4.
  • A + turned into a space. Standard base64 went into a URL query string unescaped. Use base64url, or percent-encode the value with a URL encoder.
  • Garbled accented characters. The bytes were not UTF-8 before encoding. The alphabet is not the problem here — the source encoding is.

If you just want to see what a string decodes to without writing any code, paste it into the base64 encoder and decoder. It runs entirely in your browser, handles both the standard and URL-safe alphabets, and tolerates missing padding — so a raw JWT segment or a URL-safe token decodes without you having to identify the variant first.

The bottom line: base64url is not a different encoding, it is the same six-bit-per-character scheme with two characters swapped and padding made optional, built specifically so the output survives a trip through a URL, a filename, or an HTTP header without further escaping.

Frequently Asked Questions

They share the same A-Z, a-z, 0-9 alphabet and only differ in two characters. Standard base64 uses + at index 62 and / at index 63, while base64url uses - and _ so the output is safe in URLs and filenames. base64url also commonly omits the trailing = padding.

JWTs travel in URLs, Authorization headers, and cookies, where +, /, and = cause problems. RFC 7515 requires the URL- and filename-safe alphabet with all trailing = padding omitted, which keeps the three token parts compact and transmittable without further escaping or percent-encoding.

Replace every - with + and every _ with /, then re-add = padding until the length is a multiple of 4. After that, any standard base64 decoder will accept it. Going the other way, replace + with - and / with _, then strip the trailing = characters.

You likely fed base64url (which contains - or _) into a strict standard base64 decoder, or fed standard base64 (with + or /) into a URL-safe decoder. Identify which variant you have, or use a decoder that auto-detects both alphabets and tolerates missing padding.

The encoded data is the same size character for character, since both pack 6 bits per character. base64url is only shorter when it omits the trailing = padding, saving at most two characters per encoded value. The savings are negligible except across many tokens.