Base64 vs Base64url: What Is the Difference?

Base64 turns bytes into text so they can travel through systems that only handle text. Base64url is the same idea with two characters swapped so the result is safe in a URL or file name. Mixing them up is behind many “invalid character” and “incorrect padding” errors.

What Base64 does

Base64 reads input three bytes (24 bits) at a time and writes four characters, each standing for six bits. The alphabet has 64 symbols: A–Z, a–z, 0–9, + and /. When the input length is not a multiple of three, the output is padded with one or two = signs so its length is a multiple of four.

Man   → TWFu
Ma    → TWE=
M     → TQ==

Because three bytes become four characters, Base64 output is about 33% larger than the input, plus line breaks if you use the MIME variant that wraps every 76 characters. That overhead is the price for being able to embed binary data in JSON, XML, email, HTML or a configuration file.

Base64 is an encoding, not encryption and not compression. Anyone can reverse it instantly, so it hides nothing: a “secret” stored as Base64 is as exposed as plain text. You can check any value with the Base64 decoder.

What Base64url changes

The standard alphabet contains + and /, and the padding character = is also special in places. All three cause trouble in URLs, query strings and file names, where + can mean a space, / separates path segments and = separates key from value.

Base64url (RFC 4648, section 5) fixes that with two substitutions:

  • + becomes -
  • / becomes _

Padding is usually dropped as well, because the length of the data already implies how much padding there would be. The same bytes therefore look slightly different in each form:

bytes 0xFB 0xFF 0xBE
Base64     →  +/++
Base64url  →  -_--

Base64url is the form used by JSON Web Tokens, so each segment of a token is Base64url without padding. A token that contains +, / or = has been altered somewhere along the way; the JWT debugging guide covers that case.

Which one should you use?

Match the context the value will live in:

  • Embedding binary in JSON, XML or a data URI: standard Base64. Data URIs such as data:image/png;base64,... expect the standard alphabet.
  • Anything that goes in a URL, cookie, header or file name: Base64url, so no further escaping is needed.
  • JWTs, JWS and JWE: Base64url without padding, as the specifications require.
  • Email attachments: standard Base64 with line breaks (MIME).
  • PEM certificates and keys: standard Base64 wrapped at 64 characters between BEGIN and END lines.

If you are producing a value yourself, pick Base64url unless the receiving system demands the standard form. If you are consuming one, check which alphabet it uses before decoding, because a strict decoder rejects the other one.

"Incorrect padding" and missing equals signs

Python’s base64.b64decode and urlsafe_b64decode raise binascii.Error: Incorrect padding when the input length is not a multiple of four. Unpadded Base64url from a JWT triggers this constantly. Restore the padding before decoding:

import base64

def b64url_decode(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))

Browsers’ atob is more forgiving about missing padding but strict about the alphabet: it throws on - and _. Convert them back first:

const toStd = (s) => s.replace(/-/g, '+').replace(/_/g, '/');
atob(toStd(token.split('.')[1]));

In Node.js, Buffer.from(s, 'base64url') accepts the URL-safe alphabet with or without padding, so no conversion is needed.

One more trap: a Base64 string whose length leaves a remainder of one when divided by four (such as 5 characters) can never be valid, because one leftover character holds only six bits, not a full byte. That points to truncated or corrupted data and not to a padding problem.

Unicode: why btoa throws

Base64 encodes bytes, but JavaScript strings are sequences of UTF-16 characters. btoa only accepts characters up to code point 255, so this fails:

btoa('Zoë');   // works: 'Wm/r' (Latin-1 bytes)
btoa('王芳');  // InvalidCharacterError

The first call even gives the wrong answer for UTF-8 consumers, because it encoded the Latin-1 byte for the accented letter rather than UTF-8. Encode to UTF-8 bytes first:

const bytes = new TextEncoder().encode('王芳');
const b64 = btoa(String.fromCharCode(...bytes));
const back = new TextDecoder().decode(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)));

For large inputs, spreading bytes into String.fromCharCode can overflow the call stack, so build the string in chunks or use Uint8Array.prototype.toBase64() where your runtime provides it. The same two-step rule applies when decoding: Base64 to bytes first, then bytes to text with the right character set.

Binary data, files and data URIs

Base64 is often used to move files through text-only channels. A small image can be embedded in HTML or CSS as a data URI, which saves a request but makes the document larger and stops the browser from caching the image separately. That trade-off favours tiny icons, not photographs.

When an API accepts a file as a Base64 string inside JSON, remember three limits. The request body is a third bigger than the file. The server must decode the whole string in memory before it can use it. And proxies or gateways often cap body size, so a 9 MB upload can become a 12 MB request that is rejected. For anything large, upload the file as multipart form data or to object storage with a signed URL, and send only a reference in the JSON.

When you decode, always treat the result as bytes. Writing it to a file in binary mode keeps images, PDFs and archives intact; decoding it as text only makes sense when you know the bytes are text and which character set they use.

Common mistakes to avoid

  • Treating Base64 as security. Use real encryption, or a secrets manager, for anything confidential.
  • Putting standard Base64 in a URL. The + may arrive as a space and the = may be stripped or double-escaped, which corrupts the data.
  • Embedding large files as Base64 in JSON. The 33% growth, plus the memory to hold both forms, adds up quickly; send the file separately where you can.
  • Comparing encoded strings. Different encoders may pick different padding or line wrapping for the same bytes, so decode both sides before comparing.
  • Forgetting the whitespace. Line breaks from MIME or PEM wrapping must be stripped before a strict decoder will accept the text.

When something refuses to decode, count the length, look for - or _ (Base64url) or + and / (standard), and check for stray whitespace. Those three checks solve most cases.

Frequently asked questions

What is the difference between Base64 and Base64url?

Base64url replaces + with - and / with _, and usually omits the = padding, so the output is safe in URLs and file names. The underlying encoding is identical.

Is Base64 encryption?

No. It is a reversible encoding that anyone can decode in one step, so it protects nothing. Use encryption for confidential data.

Why does my Base64 string end with = signs?

The equals signs pad the output to a multiple of four characters when the input length is not a multiple of three. One or two of them is normal; Base64url often leaves them off.

Why does btoa fail on emoji or Chinese text?

btoa only accepts characters up to code point 255. Convert the text to UTF-8 bytes with TextEncoder first, then encode those bytes.

How much bigger is Base64 than the original data?

About 33% bigger, because every three bytes become four characters. MIME line wrapping adds a little more.

Related