Upload a file
  1. base64hub
  2. Learn Base64
  3. Base64 vs Base64URL
Guide

Base64 vs Base64URL: Alphabets, Padding, Pitfalls

Base64URL is Base64 with two characters changed so the output can sit in a URL or filename untouched. The bytes are identical; the alphabets differ in two places.

The difference in one table

Both variants are defined in RFC 4648. Section 4 describes standard Base64, section 5 the “URL and Filename safe” alphabet. They share 62 characters and the same algorithm; only indexes 62 and 63 differ.

Standard Base64 (RFC 4648 §4)Base64URL (RFC 4648 §5)
Index 0–61A–Z a–z 0–9same
Index 62+-
Index 63/_
Padding= required by defaultusually omitted
Safe in URL path or querynoyes
Safe as a filenameno (/ is a path separator)yes

Here is the same input encoded both ways:

InputStandardBase64URLBase64URL, no padding
<<???>>PDw/Pz8+Pg==PDw_Pz8-Pg==PDw_Pz8-Pg
bytes fb ff+/8=-_8=-_8
ManTWFuTWFuTWFu

The last row is a reminder that many strings look identical in both variants. A string is only distinguishable if it contains +, /, - or _, or if padding is missing. When you cannot tell, a decoder that accepts both alphabets is the safe choice.

A quick way to identify a string you were handed:

  • It contains - or _: Base64URL.
  • It contains + or /: standard Base64.
  • Its length is not a multiple of 4 and it has no =: unpadded, which almost always means Base64URL.
  • It contains both + and -, or both / and _: something mixed the alphabets, or the data is not Base64 at all. The Base64 validator shows which characters are out of place.
  • It has three dot-separated parts starting with eyJ: a JWT, which is Base64URL by definition.

Why Base64URL exists

In a URL, / separates path segments, + means a space in form-encoded query strings, and = separates keys from values. Standard Base64 uses all three. Putting it in a URL means percent-encoding it, so ab+/8= becomes ab%2B%2F8%3D, which is longer, uglier and one more step to get wrong.

RFC 4648 §5 fixes this by choosing - and _, which are unreserved in URLs and legal in filenames on every common file system. The RFC also notes that padding may be skipped when the length is known, since = would itself need escaping as %3D. More on that in the padding guide.

Where each variant is used

Standard Base64:

  • MIME email attachments and encoded headers (Base64 in email)
  • PEM certificates and keys
  • data: URIs in HTML and CSS
  • HTTP Basic Auth headers
  • Kubernetes Secrets and most JSON APIs that carry files

Base64URL, usually without padding:

  • JSON Web Tokens: header, payload and signature are all unpadded Base64URL (Base64 in JWT)
  • JWK keys, JWS and JWE in general (RFC 7515–7518)
  • WebAuthn credential IDs and challenges
  • OAuth PKCE code_challenge values (RFC 7636)
  • Random URL tokens, such as Python’s secrets.token_urlsafe(), which returned a 43-character string for 32 random bytes

Converting between them

Conversion is pure character substitution, with padding handled separately. The bytes never change.

Standard to URL-safe: replace + with -, / with _, and strip trailing =. URL-safe to standard: replace - with +, _ with /, and add = until the length is a multiple of 4.

const toUrl = (b64) =>
  b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

const fromUrl = (u) => {
  const b64 = u.replace(/-/g, "+").replace(/_/g, "/");
  return b64 + "=".repeat((4 - (b64.length % 4)) % 4);
};

toUrl("PDw/Pz8+Pg==");   // "PDw_Pz8-Pg"
fromUrl("PDw_Pz8-Pg");   // "PDw/Pz8+Pg=="

Most languages also ship a direct encoder, so you rarely need the string juggling:

LanguageBase64URL encoderPadding
Pythonbase64.urlsafe_b64encode(b)kept; strip with .rstrip(b"=")
Node.jsbuf.toString("base64url")omitted
Gobase64.RawURLEncoding / base64.URLEncodingomitted / kept
JavaBase64.getUrlEncoder().withoutPadding()omitted
PHPrtrim(strtr(base64_encode($s), '+/', '-_'), '=')omitted

For decoding, Node’s Buffer.from(s, "base64") accepts both alphabets and missing padding. Python’s base64.urlsafe_b64decode needs the padding restored first. Browser atob() rejects - and _ with an InvalidCharacterError, so convert first or use Uint8Array.fromBase64 with alphabet: "base64url" where it is available. Code for other languages is on the code pages, for example Python and Java.

If you just need to switch one string, Base64 to Base64URL and Base64URL to Base64 do it in the browser.

Pitfalls

+ turns into a space

This is the most common bug. Standard Base64 placed in a query string without escaping gets mangled by form decoding, which reads + as a space:

from urllib.parse import parse_qs
parse_qs("t=ab+/8=")
# {'t': ['ab /8=']}
new URLSearchParams("t=ab+/8=").get("t");
// "ab /8="

The decoder downstream then fails on the space, or skips it and returns the wrong bytes. The fix is either Base64URL, or encodeURIComponent() on the way in. As a quick repair on the receiving side, replacing spaces back with + usually works because Base64 never contains spaces.

Mixing alphabets silently

Python’s base64.b64decode discards characters outside the standard alphabet unless you pass validate=True. Fed a Base64URL string, it drops the - and _ and decodes the rest:

import base64
base64.b64decode("PDw_Pz8-Pg==")
# b'<<\x0f\xcf\xc3\xe0'   (wrong; the input was "<<???>>")
base64.b64decode("PDw_Pz8-Pg==", validate=True)
# binascii.Error: Only base64 data is allowed
base64.urlsafe_b64decode("PDw_Pz8-Pg==")
# b'<<???>>'

The first call returns wrong bytes without raising an error. Use urlsafe_b64decode for URL-safe input, or validate=True to make the mismatch loud.

Double encoding

Two different things go by this name, and both happen in practice.

Percent-encoding twice. A standard Base64 string is escaped once by your code and once more by an HTTP client, so + becomes %2B and then %252B. The server decodes once and receives a literal %2B. Base64URL avoids the problem because there is nothing to escape.

Base64 of Base64. A value gets encoded at two layers, for example by an application and again by a framework or Kubernetes manifest tooling. Man becomes TWFu, then VFdGdQ==. The tell is that decoding gives you something that is still made of Base64 characters. Decode again and check whether the result makes sense.

Padding expectations

Some receivers require padding and others reject it. Go’s RawURLEncoding fails on input that contains =, while Python’s decoder fails on input that lacks it. When you define an API, state the variant and the padding rule in the documentation, and make your own decoder accept both.

Decode Base64URL

Paste a URL-safe string, with or without padding, and see what is inside.

Base64URL decoder

FAQ

Is Base64URL the same as URL-encoding a Base64 string?

No. Percent-encoding turns + into %2B and / into %2F, which makes the string longer. Base64URL uses a different alphabet so no escaping is needed at all.

Can a standard Base64 decoder read Base64URL?

Not reliably. Some decoders, like Node’s Buffer, accept both alphabets. Others reject - and _, and Python’s b64decode silently discards them by default, which produces wrong bytes.

Does Base64URL have padding?

RFC 4648 allows it, and Python’s urlsafe_b64encode keeps it. Most formats that use Base64URL, including JWT, drop the = because the length already defines it.

Which one should I use for tokens in URLs?

Base64URL without padding. It needs no escaping in paths, query strings, cookies or filenames.

Keep reading