The short answer: standard Base64 uses + and / for values 62 and 63. Base64URL uses - and _ instead. Both are defined by RFC 4648 and both can use = padding. Padding is omitted only when the surrounding specification permits it; JWS and JWT deliberately use unpadded Base64URL. In every case, the encoder transforms bytes—not abstract text characters—and provides no secrecy or authenticity.
The exact difference between Base64 and Base64URL
Base64 represents arbitrary bytes with printable ASCII. It reads three input bytes, or 24 bits, at a time; divides those bits into four six-bit values; and maps each value to one of 64 characters. Uppercase letters cover values 0–25, lowercase letters cover 26–51, and digits cover 52–61. The last two values are where the variants diverge.
| Property | Base64 | Base64URL |
|---|---|---|
| RFC 4648 section | Section 4 | Section 5 |
| Alphabet values 0–61 | A–Z a–z 0–9 | A–Z a–z 0–9 |
| Value 62 | + | - |
| Value 63 | / | _ |
| Pad marker | = | = |
| Typical use | MIME, certificates, general binary-to-text data | URLs, filenames, JOSE, JWS, and JWT segments |
The substitution does not change the decoded bytes. For example, bytes fb ff in hexadecimal encode as +/8= with standard Base64 and -_8= with Base64URL. After choosing the matching alphabet, both strings decode to exactly the same two bytes.
Do not call Base64URL merely “Base64” in a protocol contract. A value that happens to contain only letters and digits looks valid under both alphabets, so the text alone cannot always identify its variant. The field definition, media format, or protocol must say which alphabet and padding policy apply.
How Base64 padding works
The = character is not encoded payload. It completes the final four-character output quantum when the input byte count is not divisible by three. With padded Base64, the output length is 4 × ceil(input bytes / 3). The encoder can encounter only three ending cases:
| Input bytes modulo 3 | Useful Base64 characters | Padding | Example |
|---|---|---|---|
| 0 | 4 per three bytes | None | foo → Zm9v |
| 1 | 2 for the final byte | == | f → Zg== |
| 2 | 3 for the final two bytes | = | fo → Zm8= |
RFC 4648’s general rule is to include appropriate padding unless the specification that uses Base64 explicitly says otherwise. Padding may be omitted when the decoder knows the data length implicitly. Therefore, “Base64URL is always unpadded” is false. Base64URL defines the URL-safe alphabet; a separate protocol defines whether its values retain or omit =.
An unpadded decoder can restore the required characters from the encoded length. A length divisible by four needs no padding, a remainder of two needs ==, and a remainder of three needs =. A remainder of one is impossible for a valid encoding of whole bytes and should be rejected. Blindly appending a fixed == can conceal malformed input, so calculate the remainder and validate it.
Protocol rules win. MIME may permit line wrapping and ignored whitespace; JWS permits neither whitespace nor padding in its Base64URL values. A general-purpose decoder should offer an explicit mode instead of silently combining every permissive behavior.
Canonical encoding and zero pad bits
Padding characters are visible, but another kind of padding exists inside the last six-bit symbol. When only one input byte remains, the second output symbol carries two real bits followed by four unused bits. When two bytes remain, the third output symbol carries four real bits followed by two unused bits. RFC 4648 requires a conforming encoder to set these unused pad bits to zero.
Zero pad bits create one canonical string for one byte sequence. The byte ff canonically encodes as /w==. A permissive decoder might also turn /x== into ff because it discards the nonzero unused bits, but /x== is not canonical. RFC 4648 allows decoders to reject such encodings, and security-sensitive protocols often benefit from doing so.
A strict validation pattern is: check the expected alphabet and padding shape, decode, re-encode with the required variant and padding policy, then compare the canonical text to the input. This catches nonzero pad bits as well as alternate spellings. Perform the comparison on the original protocol representation; do not first trim whitespace, swap alphabets, or add and remove arbitrary punctuation unless that normalization is explicitly part of the protocol.
Base64 encodes bytes, while text needs a character encoding
Base64 does not know about Unicode characters, JSON, images, or files. Its input is an octet sequence. To encode text, first choose a character encoding—normally UTF-8—to turn characters into bytes. To recover text, Base64-decode back to bytes and then UTF-8-decode those bytes. If the bytes are a PNG, compressed archive, signature, or encrypted ciphertext, treating them as UTF-8 text is a category error.
ASCII can hide this boundary because each ASCII character becomes one UTF-8 byte. Non-ASCII text reveals it. The check mark ✓ is one Unicode character but three UTF-8 bytes, e2 9c 93, whose Base64 form is 4pyT. An implementation that encodes a language runtime’s 16-bit code units or assumes one character equals one byte will produce the wrong result.
In browsers, btoa() and atob() operate on a “binary string” whose character values must fit in one byte; they are not UTF-8 text codecs. Calling btoa("✓") throws in conforming browsers. Use TextEncoder before Base64 encoding and TextDecoder after Base64 decoding. For untrusted data, consider new TextDecoder("utf-8", { fatal: true }) so invalid UTF-8 becomes an error instead of replacement characters.
Browser JavaScript: encode UTF-8 and decode Base64URL
The helpers below keep the byte/text boundary explicit, convert alphabets intentionally, restore only the required padding, and reject an impossible Base64URL length. Chunking avoids passing a very large byte array as one function call.
function bytesToBase64(bytes) {
let binary = "";
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
}
return btoa(binary);
}
function base64ToBytes(value) {
const binary = atob(value);
return Uint8Array.from(binary, char => char.charCodeAt(0));
}
function textToBase64Url(text) {
return bytesToBase64(new TextEncoder().encode(text))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
function base64UrlToBytes(value) {
if (!/^[A-Za-z0-9_-]*$/.test(value)) {
throw new TypeError("invalid Base64URL alphabet");
}
if (value.length % 4 === 1) {
throw new TypeError("impossible Base64URL length");
}
const padLength = (4 - (value.length % 4)) % 4;
const base64 = value.replace(/-/g, "+").replace(/_/g, "/")
+ "=".repeat(padLength);
return base64ToBytes(base64);
}
const encoded = textToBase64Url("Hello, ✓");
const decoded = new TextDecoder("utf-8", { fatal: true })
.decode(base64UrlToBytes(encoded));
This decoder is intentionally for unpadded Base64URL. If a field permits padding, validate a grammar that allows exactly the appropriate trailing = characters instead of deleting all equals signs. Also place a size limit before atob(); encoding increases data size, but decoding an attacker-controlled multi-megabyte string can still consume meaningful memory.
Node.js and Python examples
Modern Node.js exposes both names directly through Buffer. The base64url output form omits padding, which matches JOSE usage. A decoded buffer remains bytes until toString("utf8") interprets it as text.
const input = Buffer.from("Hello, ✓", "utf8");
const standard = input.toString("base64");
const urlSafe = input.toString("base64url");
const rawBytes = Buffer.from(urlSafe, "base64url");
const text = rawBytes.toString("utf8");
console.log({ standard, urlSafe, text });
Python’s base64 module offers URL-safe helpers, but strict validation is clearest with b64decode, an alternate alphabet, and validate=True. The padding formula (-len(value)) % 4 adds zero, one, two, or three characters; the explicit remainder-one rejection prevents the otherwise suspicious three-padding case.
import base64
def encode_base64url(text: str) -> str:
raw = text.encode("utf-8")
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
def decode_base64url(value: str) -> bytes:
if len(value) % 4 == 1:
raise ValueError("impossible Base64URL length")
padded = value + "=" * ((-len(value)) % 4)
return base64.b64decode(padded, altchars=b"-_", validate=True)
encoded = encode_base64url("Hello, ✓")
raw = decode_base64url(encoded)
text = raw.decode("utf-8", errors="strict")
Library defaults differ in how they treat whitespace, missing padding, extra padding, and the other alphabet. Test the exact runtime version used in production and keep protocol validation separate from a forgiving convenience decoder. Interoperability is easiest when encoders emit one canonical representation and decoders accept only the forms the contract names.
Why JWTs use unpadded Base64URL
A typical signed JSON Web Token is a JWS Compact Serialization with three dot-separated segments:
BASE64URL(UTF8(protected header))
.
BASE64URL(payload bytes)
.
BASE64URL(signature bytes)
RFC 7515 defines its Base64URL encoding as the RFC 4648 URL-safe alphabet with all trailing = omitted and with no line breaks, whitespace, or other added characters. The header is usually JSON encoded as UTF-8. For a JWT, the payload is normally a UTF-8 JSON claims set. The signature segment is arbitrary signature or MAC bytes, not text.
The signing input is the ASCII bytes of the original encoded protected-header segment, a literal period, and the original encoded payload segment. That exact representation matters. If you decode the JSON, pretty-print it, reorder properties, normalize escapes, and Base64URL-encode it again, you have changed the signing input even when the parsed JSON data appears equivalent. Verify against the original segments.
Decoding a JWT does not verify it. Anyone can construct Base64URL text. Seeing readable claims—or a header that says "alg":"RS256"—proves nothing about who issued the token. Authentication requires cryptographic verification with a trusted key, an allowed algorithm, and application checks for claims such as issuer, audience, expiration, and not-before time.
Some compact strings with dots are encrypted JWEs rather than three-part signed JWS objects; their compact form has five segments. Do not assume every JWT exposes readable JSON or that every three-part token is valid. Parse structure, enforce limits, reject unexpected algorithms, and keep decoding status separate from verification status.
Use the LiveParse JWT Decoder for strict segment and claim inspection, then read JWT decode vs verify for the algorithm, key, issuer, audience, and authorization boundary.
Base64 in URLs, filenames, and cookies
Standard Base64 characters collide with common transport syntax. A slash can be a path separator. In form-style query parsing, a plus sign is often decoded as a space. An equals sign can act as a name/value separator or require percent-encoding. Base64URL replaces the first two problem characters, and protocols often omit padding to avoid the third.
Base64URL is therefore a good alphabet for path components, query values, filenames, and conservative cookie values—but it is not a universal escaping function. The surrounding URL or cookie grammar still applies. Percent-encode a URL component when required, use a proper URL builder, and do not concatenate untrusted strings into paths. For cookies, respect the framework’s serializer, per-cookie size constraints, and security attributes. Unpadded Base64URL reduces syntax friction; it does not make a cookie secret or tamper-proof.
If an external API requires standard padded Base64 in a URL, follow that contract and percent-encode the value as a URL component. Do not swap alphabets merely because the value travels over HTTP. Conversely, do not feed a JWT segment to a decoder configured only for standard padded Base64 and hope its permissive fallback guesses correctly.
Security and validation rules
Base64 is reversible encoding, not encryption. It has no key and provides no confidentiality. It also provides no integrity: an attacker can alter the encoded text and produce different decoded bytes. Use authenticated encryption when data must remain secret, and use a signature or MAC when recipients must detect changes. Those cryptographic results may themselves be carried as Base64 or Base64URL.
Encoding credentials, access tokens, personal data, or private keys does not make them safe to paste into a public tool, put in a URL, store in logs, or commit to source control. URLs can appear in browser history, analytics, server logs, screenshots, and referrer data. Prefer local processing, minimize retention, and treat every decoded result according to the sensitivity of the original bytes. Review the LiveParse privacy approach before working with sensitive input.
At an untrusted boundary, validate before allocating or interpreting:
- Choose one declared variant and padding policy instead of guessing from content.
- Set a maximum encoded length and a maximum decoded length. Base64 expands data by roughly one third.
- Reject characters outside the selected alphabet unless the governing specification explicitly permits them.
- Reject padding in the middle, too much trailing padding, and the impossible unpadded length remainder of one.
- For canonical protocols, verify zero pad bits by re-encoding and comparing the required representation.
- Keep bytes as bytes until a content type says they are UTF-8, JSON, an image, or another format.
- After decoding structured content, apply that format’s own parser, schema, size, nesting, and security checks.
RFC 4648 says decoders must reject non-alphabet characters unless a referring specification explicitly chooses a liberal policy. Silently discarding arbitrary whitespace or punctuation can create covert channels and inconsistent validation between services. A strict boundary parser followed by explicit content handling is safer than “decode whatever the library accepts.”
Which variant should you use?
- Use standard Base64 when the file format, API, PEM/MIME container, or existing field explicitly requires it.
- Use Base64URL when designing a new value for URLs, filenames, JOSE, or another token-oriented ASCII context.
- Keep padding by default for RFC 4648 Base64 unless your protocol says it may be omitted.
- Omit padding for JWS and JWT because RFC 7515 requires the unpadded form.
- Specify UTF-8 when your application starts with text; specify raw bytes when it does not.
- Document strictness for whitespace, mixed alphabets, padding, canonical bits, empty input, and maximum size.
Need to inspect or create a value? The LiveParse Base64 Decoder and Base64 Encoder make the alphabet, padding policy, UTF-8 step, and byte output explicit instead of guessing silently.
Open the Base64 DecoderFrequently asked questions
Can one decoder accept both Base64 and Base64URL?
Some libraries do, but a protocol validator usually should not. When a value contains +, /, -, or _, its alphabet may be obvious; an alphanumeric-only value is ambiguous. Accepting both can hide an integration error and create different canonical strings for the same bytes. Select a mode from the field’s contract.
Does Base64URL always remove padding?
No. RFC 4648 defines = for Base64URL too. Its general rule is to include padding unless another specification explicitly permits omission. JWS is such a specification: it defines Base64URL with trailing padding omitted.
Why does btoa() fail on emoji or non-English text?
btoa() expects each JavaScript string element it processes to represent a byte-sized value; it does not perform UTF-8 encoding. Convert the text with TextEncoder, encode those bytes, and reverse the sequence with byte decoding plus TextDecoder.
Can I safely put standard Base64 in a URL?
Yes when the API requires it and you encode it as a URL component, but raw +, /, and = can interact with URL syntax or form decoders. Base64URL was designed to reduce those conflicts. The endpoint contract remains authoritative.
How can I tell whether missing padding is valid?
Check the governing format first. For a permitted unpadded value, length modulo four may be zero, two, or three. A remainder of one cannot represent a whole-byte Base64 encoding and is malformed. Restore only the calculated padding before using an API that expects it.
Why can two Base64 strings decode to the same bytes?
A permissive decoder may ignore whitespace, excess padding, a mixed alphabet, or nonzero unused pad bits. Canonical RFC 4648 encoding uses the correct alphabet, padding policy, and zero pad bits. Re-encode the bytes and compare when a unique textual representation matters.
Is a decoded JWT trustworthy?
No. Base64URL decoding only reveals bytes. Trust requires signature or MAC verification with an expected algorithm and trusted key, followed by application-specific claim validation. A readable payload can be completely forged.
Primary standards
- RFC 4648: Base-N Encodings — alphabets, padding, rejection rules, canonical pad bits, and Base64URL.
- RFC 7515: JSON Web Signature — unpadded Base64URL and the exact JWS signing input.
- RFC 7519: JSON Web Token — JWT structure, claims, validation, and security considerations.
- WHATWG Encoding Standard — UTF-8 algorithms,
TextEncoder, andTextDecoder. - RFC 3986: URI Generic Syntax — reserved characters and percent-encoding rules for URI components.