← Blog Β· May 23, 2026 Β· dev, encoding

Base64 vs URL-Safe Base64: When the Difference Matters

Standard Base64 (RFC 4648 section 4) uses 64 characters: A-Z, a-z,0-9, plus + and /. URL-safe Base64 (RFC 4648 section 5) swaps the last two for - and _, and usually drops the trailing= padding. That two-character difference is the entire reason JWTs, OAuth tokens, and image URLs work β€” and the entire reason naive decoders break on them.

The numbers

Base64 inflates payload size by exactly 4/3 β€” three bytes in become four characters out. A 1 KB image becomes ~1366 characters. The output length is always a multiple of 4 in standard Base64 (padded with =). In URL-safe Base64 the padding is typically omitted, so the length is whatever it works out to: a 32-byte SHA-256 hash becomes 43 characters URL-safe, or 44 standard.

The two alphabets side by side

PositionStandard (RFC 4648 Β§4)URL-safe (Β§5)
0–25A–ZA–Z
26–51a–za–z
52–610–90–9
62+-
63/_
Padding=often omitted

Why URL-safe exists

Put a standard Base64 string into a URL and the / looks like a path delimiter, the + means space in form-urlencoded query strings, and the= is reserved. The string aGVsbG8= in a URL becomesaGVsbG8%3D after percent-encoding β€” longer, uglier, and a decoder that doesn't un-percent-encode first will fail. URL-safe Base64 avoids all three reserved characters and the padding, so the string drops into a URL untouched.

The same bytes, two encodings

Input bytes (hex):   FB EF FF
Standard Base64:     ++//
URL-safe Base64:     --__

Input:               { "alg": "HS256" }   (16 bytes)
Standard:            eyAiYWxnIjogIkhTMjU2IiB9
URL-safe:            eyAiYWxnIjogIkhTMjU2IiB9   (same β€” no + / present)

Notice β€” most short ASCII strings encode identically in both alphabets, because they never produce a + or /. The difference only appears when the input has certain byte patterns. That is why bugs hide: your test data works in both modes, then a binary payload arrives and one decoder explodes.

JWT uses URL-safe Base64. Always.

Every JWT is three URL-safe Base64 strings joined by . dots. RFC 7515 mandates the URL-safe variant with no padding β€” section 2 says it explicitly. If your decoder is rejecting JWTs from a third party, the cause is almost always one of:

  1. Standard Base64 decoder used on URL-safe input (chokes on - or _).
  2. Decoder expects padding and the token has none.
  3. The token was double-encoded somewhere in transit.

Paste any JWT into our JWT decoder to see the three URL-safe Base64 parts decoded to JSON.

Converting between the two variants

// Standard β†’ URL-safe
function toUrlSafe(b64: string): string {
  return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// URL-safe β†’ Standard (re-pad to multiple of 4)
function fromUrlSafe(b64url: string): string {
  let s = b64url.replace(/-/g, '+').replace(/_/g, '/');
  while (s.length % 4) s += '=';
  return s;
}

// In modern Node, prefer the native encodings
Buffer.from(data, 'base64url').toString('utf8');
Buffer.from('hello').toString('base64url');

Decision: which one should you emit?

You are putting Base64 into…Use
A URL path or query stringURL-safe, no padding
A JWT, OAuth token, magic linkURL-safe (mandated)
An HTTP header valueURL-safe (avoids accidental space-to-plus)
An email body, MIME attachmentStandard with padding (RFC 2045)
A JSON fieldEither works β€” pick one and document it
A data: URI for an imageStandard (per RFC 2397)
A filenameURL-safe β€” / in a filename is a different file

The padding question

Padding (=) exists so that a Base64 string can be concatenated and still be decodable in 4-character chunks. For one-shot encoding it is decorative. You can strip it when emitting and re-pad on decode by appending = until length is a multiple of 4. Most URL-safe contexts omit it. Most MIME contexts require it.

One more gotcha: line wrapping

RFC 2045 (MIME) Base64 wraps at 76 characters. RFC 4648 Base64 does not. If you paste a MIME-style Base64 blob into a JSON parser or a URL, the newlines break it. Strip them with.replace(/\s/g, '') before decoding.

Tools

← All blog posts