Upload a file
  1. base64hub
  2. Learn Base64
  3. Padding (=)
Explainer

Base64 Padding: What the = Signs Do

The = at the end of Base64 marks a short final block. Some formats drop it, some decoders insist on it, and restoring it takes one line of code.

Why Base64 needs padding

Base64 converts 3 bytes into 4 characters (the full algorithm is in how Base64 works). When the input length is not a multiple of 3, the last block is short:

Bytes in last blockCharacters of dataPaddingExample
34noneMan → TWFu
23=Ma → TWE=
12==M → TQ==

The = keeps the output length a multiple of 4. That made life easier for decoders written in the MIME era, which could read input in fixed 4-character groups and know where the data stops without tracking the total length.

Strictly speaking the information is redundant. A 3-character final group can only mean 2 bytes, and a 2-character group can only mean 1 byte, so a decoder that counts characters needs no = at all. That is why unpadded Base64 is possible and why many formats use it.

There is one case where padding carries real information: concatenation. If two padded strings are glued together, the = in the middle marks where the first one ends. Decoders handle that badly, though. Feeding TQ==TQ== (two encoded Ms) to Python’s default decoder returned b'M\x04\xd0', Node’s Buffer returned just M, and atob threw. Decode each piece separately instead.

When padding is optional

RFC 4648 §3.2 says encoders MUST add padding unless the specification that uses Base64 explicitly says otherwise. Several do:

  • JWT and JOSE (RFC 7515) use Base64URL with the padding removed. A JWT header like eyJhbGciOiJIUzI1NiJ9 has no = even when the length is not a multiple of 4. See Base64 in JWT.
  • URLs and filenames often carry unpadded Base64URL, because = has a meaning in query strings and would need to be escaped as %3D. The Base64 vs Base64URL page covers the full difference.
  • WebAuthn and OAuth PKCE (the code_challenge in RFC 7636) also use unpadded Base64URL.

Standard Base64 in email, PEM files, data URIs and most JSON APIs is padded. When in doubt, produce padded output for standard Base64 and unpadded output for Base64URL, and make your decoder accept both.

Decoders that require padding

Behavior varies more than people expect. These are the results of decoding TWE (unpadded Ma) with each library’s default decoder:

Language / toolCallResult for TWE
Python 3.13base64.b64decodebinascii.Error: Incorrect padding
Python 3.13base64.urlsafe_b64decodeIncorrect padding as well
.NETConvert.FromBase64StringFormatException
Gobase64.StdEncodingerror; use RawStdEncoding for unpadded input
Gobase64.RawStdEncodingMa, but rejects TWE=
RubyBase64.strict_decode64ArgumentError: invalid base64
RubyBase64.decode64, urlsafe_decode64Ma
Java 21Base64.getDecoder().decodeMa
Node.jsBuffer.from(s, 'base64'), atobMa
PHP 8.4base64_decode (also strict mode)Ma
GNU coreutils 9.7base64 -dMa
OpenSSL 3.5openssl base64 -dempty output, exit code 0

Note the OpenSSL row: it printed nothing and still reported success, so a script that pipes unpadded Base64 through openssl base64 -d silently produces an empty file. In the same tests it also returned nothing for a single line without a trailing newline; passing -A fixed that case.

Go is the strictest in the other direction: RawStdEncoding refuses a string that does have padding. If you accept input from several sources, strip = and use the raw encoding, or add the padding and use the standard one.

Language-specific examples are on the code pages, for instance Base64 in Python and Base64 in Go.

How to restore missing padding

The rule depends only on the length of the string after removing whitespace:

len % 4What to appendMeaning
0nothingcomplete blocks
2==last block holds 1 byte
3=last block holds 2 bytes
1cannot be fixedtruncated, or a stray character

A remainder of 1 is never valid. One Base64 character carries 6 bits, which is not enough for a byte, so no amount of padding makes it decodable. If you see this, look for a missing character, an extra character, or a newline that got counted.

The fix is one expression in most languages:

import base64
s = "eyJzdWIiOiIxMjM0In0"            # 19 chars, 19 % 4 == 3
padded = s + "=" * (-len(s) % 4)     # adds one "="
print(base64.urlsafe_b64decode(padded))
# b'{"sub":"1234"}'
const s = "eyJzdWIiOiIxMjM0In0";
const padded = s + "=".repeat((4 - (s.length % 4)) % 4);
// "eyJzdWIiOiIxMjM0In0="

The Base64 validator reports the length and padding state of a string, and Repair Base64 adds the missing =, strips whitespace and quotes, and converts URL-safe characters in one step.

Non-canonical encodings

Padding has a quieter relative: the unused bits in the last data character. When Ma is encoded, its 16 bits fill two and a half 6-bit groups. The third character carries the last 4 bits of a plus 2 filler bits, and RFC 4648 §3.5 says the encoder must set those filler bits to zero. That gives E (000100).

A decoder ignores the filler bits, so TWF= (where F is 000101, filler bits 01) decodes to the same Ma. Python, Node, Java and PHP all accepted TWF= and returned Ma in our tests. Similarly TR== decodes to M just like TQ==.

Strings like TWF= are called non-canonical. They matter in a few places:

  • Signatures and hashes over Base64 text. Two different strings decode to the same bytes, so comparing Base64 strings to check whether data is equal can give the wrong answer. Compare decoded bytes.
  • Deduplication and caching keys. Same problem: normalize by decoding and re-encoding.
  • Strict parsers. RFC 4648 lets decoders reject non-zero filler bits. Go’s StdEncoding.Strict() and GNU base64 -d both reject TWF=.

Extra padding is a related oddity. TWE== has one = too many. Python and Node’s Buffer accepted it, atob threw InvalidCharacterError.

Padding when encoding in chunks

If you Base64 a large file in pieces, every piece except the last must be a multiple of 3 bytes. Otherwise each piece gets its own padding and the joined output is wrong. Encoding hello world!! in 4-byte chunks gave aGVsbA==byB3bw==cmxkIQ==IQ==; in 3-byte chunks it gave aGVsbG8gd29ybGQhIQ==, identical to encoding the whole thing at once. For MIME line wrapping, 57 bytes per line is the natural chunk size, since 57 bytes become exactly 76 characters.

Fix padding automatically

Paste a string with missing or broken padding and get valid Base64 back.

Base64 repair

FAQ

Can I just delete the = signs from Base64?

The data survives, because the length of the string already tells a decoder how many bytes the last block holds. Whether the result still decodes depends on the receiver: Base64URL in JWTs expects no padding, while Python’s b64decode and .NET’s Convert.FromBase64String reject it.

Why does my Base64 string have no = at the end?

Either the input length was a multiple of 3 bytes, so no padding was needed, or the producer uses unpadded Base64, which is common for Base64URL in tokens and URLs.

How do I fix an Incorrect padding error in Python?

Append '=' * (-len(s) % 4) before decoding. If the string is URL-safe, also use base64.urlsafe_b64decode. If the length mod 4 is 1, padding will not help: the string is truncated or has a stray character.

Can a Base64 string end with three = signs?

No. A final block holds 1 or 2 leftover bytes, which produce == or =. Three = would mean a block with no data, which does not exist.

Keep reading