Do you have to deal with Base64 format? Then this site is perfect for you! Use our super handy online tool to encode or decode your data.

Base64 Decoding in PHP: A Complete Guide

It shows up in a support ticket, an API log, a config file, or the middle of a URL: a long string of letters, digits, the occasional + or /, and maybe an = or two at the end. You recognize it in a heartbeat. Base64 is a binary-to-text format: it rewrites every three bytes of raw data as four characters drawn from a 64-letter alphabet, and a couple of = signs finish off the tail when the byte count is not a multiple of three. Decoding is the shrinking direction of that trade: four characters go back in, three bytes come out. The main page of this site walks through the format step by step, so this article spends its energy where it belongs: on the PHP side of the job.

The headline news first. PHP has shipped a Base64 decoder in its core since PHP 4. base64_decode() needs no extension, no Composer package and no configuration, and it runs everywhere PHP runs. The less good news: its default mood quietly swallows corrupted input and hands you garbage without a word. The good news gets better: one flag ($strict) turns the function into a proper gatekeeper, and once you know how to pick the mood, prove the input is real, and translate the bytes back into meaning, Base64 stops being a source of mystery bugs and becomes a routine you can automate.

A quick note on size: decoding shrinks data by about a quarter (three bytes out for every four characters in), so the output always fits in less memory than the input. You never have to worry about a decode blowing up. Now let us meet the tool.

The Function That Does The Job

Here is the complete signature, exactly as modern PHP reports it:

base64_decode(string $string, bool $strict = false): string|false

Three words in that line do all the work. $string has no size limit: a megabyte decodes in well under a millisecond, so nothing stops you from decoding an entire file in one call. The return type states the whole contract: either a string of decoded bytes, or false. There are no exceptions, no error codes, no second channel. false is the only signal you get, so checking for it is part of the job. And one sentence from the manual deserves to be memorized: the returned data may be binary. The moment the result holds a PNG, a ZIP, or a hash, it is not a "text string" in any loose sense, and PHP will happily let you treat it as one anyway. That flexibility is a superpower and a trap, and the sections below keep it in check.

A fast pass over the version tags, because inherited code makes a habit of assuming things. The function has been in the core since PHP 4. Its $strict parameter arrived in PHP 5.2.0, in November 2006. Since PHP 8.0 the signature carries real native types (the string and bool you see above, plus the string|false return), so IDEs and static analyzers finally know the function can fail. Since PHP 8.1, passing null raises a deprecation notice; if you mean "nothing", write '' explicitly:

$decoded = base64_decode('');
var_dump($decoded); // string(0) ""

Strict Mode Or Silent Cleanup

The $strict flag is a switch between two very different personalities. Off (the default), the decoder is a friendly forgetter: every character outside the Base64 alphabet is silently discarded, the rest is decoded, and nobody is told. The manual puts it bluntly: otherwise, invalid characters will be silently discarded. On, the decoder is a gatekeeper: the first character it does not recognize earns the whole payload a false.

Here is the damage report. Every row below is real behavior of base64_decode() on PHP 8.x:

Input Lenient (default) Strict
Zm9vYmFy, clean "foobar" "foobar"
Zm9v\r\nYmFy, CRLF mid-string "foobar" "foobar"
" Zm9vYmFy ", spaces at both ends "foobar" "foobar"
Zm9v\x0bYmFy, vertical tab "foobar" false
Zm9v\x00YmFy, embedded NUL byte "foobar" false
V@hpcy, stray @ 3 bytes of garbage false
Zm9vY, five characters "foo", last char dropped false
Z, a single letter "", an empty string false
=Zm9, padding up front "fo" false
Zm9vYmFy==, pads after a full group "foobar" false
Zm9vYmFy==A, data after the pads "foobar" false
Zm9vYmF, seven chars, no pads "fooba" "fooba"

Three rows deserve a second look. The V@hpcy row shows why lenient mode is dangerous anywhere the input is untrusted: the stray @ does not stop the decode, it just vanishes, and the three bytes that come out mean nothing. The single Z row shows that an empty result proves almost nothing; a one-character payload "decodes" into an empty string without failing. The Zm9vYmFy==A row shows the decoder happily ignoring data that appears after the padding, which is how a truncated or tampered payload can look perfectly fine.

What does strict mode still let through? Exactly four whitespace characters: space, tab, carriage return and line feed, in any position, even right next to the = signs. That is deliberate. MIME-wrapped email payloads carry CRLF line breaks inside the encoded stream, and strict mode chews through them without preprocessing (the email section below explains why). Everything else that is not an alphabet character, from NUL bytes to vertical tabs, earns a false.

One quirk sets PHP apart from stricter colleagues: the 8-character payload Zm9vYmF= (seven letters plus one pad, a grouping the Base64 standard does not recognize) decodes to "fooba" even in strict mode. Other platforms' strict decoders reject it. If your PHP side and a partner system disagree about an edge-case payload, this is usually where to look.

The standard agrees with the strict mood. RFC 4648, section 3.3, says implementations must reject encoded data that contains characters outside the alphabet, unless the surrounding specification says otherwise (MIME is the classic "say otherwise" case). The same section explains why: non-alphabet characters can be exploited as a covert channel, hiding information in characters your decoder throws away, and they have been used to trigger decoder bugs. If your input comes from the outside world, strict mode is not a style choice. It is what the standard asks for.

Proving A Payload Is Base64

A decoder that can fail quietly deserves a validation pipeline in front of it. Three layers, each catching what the others miss.

Layer one is a shape check with a regular expression: alphabet characters only, and at most two pads at the very end.

$shapeLooksPlausible = preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $payload) === 1;

The regex catches obvious garbage (stray spaces, @ signs, a pad in the middle of the string) before anything else runs. It is not a validator, though: it cannot see that Zm9vYmFy= is nine characters with one pad, which strict mode also refuses. That is exactly why layer two exists. Strict decoding is the only check that understands Base64 semantics, so it gets the final word.

Layer three is the one everyone forgets: handle false explicitly, because it is the only signal you get.

function decode_payload(string $payload): string
{
  $clean = str_replace(["\r", "\n"], '', $payload);
  $decoded = base64_decode($clean, true);
  if ($decoded === false) {
    throw new InvalidArgumentException('Not a valid Base64 payload.');
  }
  return $decoded;
}

The str_replace() up front is optional comfort: strict mode already tolerates CRLF, but stripping it keeps any length math you do later clean, because a clean payload's character count is always a multiple of four. (One more than a multiple of four, like five or nine, is impossible in Base64, and strict mode will refuse it.) Note that the function never throws on its own; the check is yours to write.

URL-Safe Base64

In the wild you will meet a second alphabet, and it is the one that bites. Standard Base64 uses + and /, two characters that are trouble in URLs: a + in a query string gets interpreted as a space before PHP ever sees it, and / is a path separator. RFC 4648, section 5, defines the fix: the URL and filename safe alphabet, where + becomes -, / becomes _, and the trailing = padding is usually dropped to save characters. The RFC is adamant that this "should not be regarded as the same as the base64 encoding", and the name you will hear most is base64url. JSON Web Tokens, OAuth state parameters, API session ids and video site URLs all live in this dialect.

The decoder side is two steps: swap the alphabet back, then restore any missing padding. Here is the helper you will end up reusing everywhere:

function base64url_decode(string $data): string|false
{
  $standard = strtr($data, '-_', '+/');
  $missing = strlen($standard) % 4;
  if ($missing !== 0) {
    $standard .= str_repeat('=', 4 - $missing);
  }
  return base64_decode($standard, true);
}
var_dump(base64url_decode('aGk_PnRoZXJl')); // string(7) "hi>?there"

Modern PHP is a helpful tenant here: it fills in missing padding for you, so the explicit restore is belt and braces (and it makes your code readable to older PHP versions). The direction of danger is one-way. If you feed URL-safe text into the standard decoder in lenient mode, the - and _ characters are simply not in the standard alphabet, so they are discarded. Your output comes out shorter than it should be, with no error, no notice, nothing. Always run the strtr() swap first, or better, always go through the helper.

One honest caveat: if a URL-safe payload happens to contain neither - nor _, the two alphabets are byte-identical for that particular data, and it does not matter which decoder you used. The danger appears only when those characters are present, because that is the only place the alphabets differ.

Text, Bytes And Character Sets

Base64 has no idea what your bytes mean, and PHP's decoder inherits that blindness. The codec is charset-blind: it hands back the same 8-bit values that went in, whether they are UTF-8 text, Windows-1252 text, a JPEG, or a hash. PHP itself is on the same page: a string is a sequence of bytes, nothing more. The moment you want to display the result or compare it against other text, someone has to answer two questions: is this text at all, and if so, in which charset?

The practical test has two steps. Binary almost always announces itself with NUL and low control bytes, and text that is not valid UTF-8 is the second bucket. The mbstring extension (bundled with standard PHP builds) gives you the strict UTF-8 check:

function looks_binary(string $bytes): bool
{
  if ($bytes === '') {
    return false;
  }
  if (strpbrk($bytes, "\x00\x01\x02\x03\x04") !== false) {
    return true;
  }
  return !mb_check_encoding($bytes, 'UTF-8');
}
var_dump(looks_binary("\x89PNG\r\n\x1a\n...png body")); // bool(true)
var_dump(looks_binary("héllo wörld, 日本語"));          // bool(false)

When the payload is text in a legacy charset, convert it before it touches your HTML. Windows-1252 is the most common legacy encoding for web and desktop data, and the difference between it and plain ISO-8859-1 decides whether byte 0x93 is a curly quote or an invisible control character:

// "café" in Windows-1252: the é is a single byte, 0xE9
$legacy = base64_decode('Y2Fm6Q==', true);
$utf8 = mb_convert_encoding($legacy, 'UTF-8', 'Windows-1252');
var_dump($utf8); // string(5) "café": the é is two UTF-8 bytes now

A warning about the famous mb_detect_encoding(): the PHP manual itself says automatic detection "can never be entirely reliable", and compares it to decrypting a message without the key. Feed it a Windows-1252 "café" and it may say Windows-1252; feed it a PNG header and it can happily say Windows-1252 again, because the ISO-8859 family of charsets is defined for every possible byte value and can therefore match anything. Treat detection as a last resort, trust a declared charset (a header, a config line, a database collation) whenever one exists, and default the rest to UTF-8 or binary.

When The Payload Is A File

The most common file job is the inverse of what some export routine did: a .b64 text file arrives, and you need the original file back. With strict decoding and a false check, this is already production-shaped:

$encoded = file_get_contents('/var/www/uploads/blob.b64');
$decoded = base64_decode($encoded, true);
if ($decoded === false) {
  http_response_code(400);
  exit('That upload is not valid Base64.');
}

PHP strings are just bytes, so nothing in this path cares whether the payload is a text file, a ZIP archive, or a video. The size math works in your favor: the decoded output is three quarters the length of the encoded input, so decoding never makes memory worse.

A good habit is to let the bytes announce themselves before you trust any label. The finfo class (the fileinfo extension, bundled with standard PHP builds) tells you what the data actually is:

$mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($decoded);
var_dump($mime); // string(9) "image/png"
$extensions = ['image/png' => 'png', 'application/pdf' => 'pdf', 'application/zip' => 'zip'];
$ext = $extensions[$mime] ?? 'bin';
$target = '/var/www/uploads/file-' . bin2hex(random_bytes(4)) . '.' . $ext;
file_put_contents($target, $decoded);

That last step matters more than it looks. A payload that claims to be an image but decodes to something else is exactly the kind of thing a second opinion catches. And if you serve the restored file back to a browser later, the Content-Type you send should come from the same finfo check, not from the file name.

Data URIs, The Clipboard Format

A favorite arrival: someone pastes an image into a form, and the front end hands you a full data URI: data:image/png;base64,iVBORw0KGgo.... RFC 2397 defines the shape: data:, an optional media type, an optional ;base64 flag, a comma, and then the data. When the flag is present the payload is Base64; when it is absent, the payload is percent-encoded plain text, rarer but legal. If the media type is omitted, the default is text/plain;charset=US-ASCII. Why Base64 here at all? Because a URI cannot safely contain raw bytes or commas, and Base64 gives you one alphabet that needs no escaping.

function split_data_uri(string $uri): ?array
{
  if (!str_starts_with($uri, 'data:') || !str_contains($uri, ',')) {
    return null;
  }
  $meta = substr($uri, 5, strpos($uri, ',') - 5);
  $payload = substr($uri, strpos($uri, ',') + 1);
  $isBase64 = str_ends_with($meta, ';base64');
  $mime = $isBase64 ? substr($meta, 0, -7) : $meta;
  if ($mime === '') {
    $mime = 'text/plain;charset=US-ASCII';
  }
  return [$mime, $isBase64, $payload];
}
$uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ';
[$mime, $isBase64, $payload] = split_data_uri($uri);
var_dump($mime); // string(9) "image/png"

Two pitfalls live in this format. The missing ;base64 flag is the first: a legal data URI without the flag carries a percent-encoded payload, and running it through base64_decode() produces garbage. The second is the claimed media type: it is a hint from the sender, not a fact. The finfo check from the file section is your fact. And remember the RFC's own advice that data URIs are only useful for short values; a multi-megabyte image inside a URL is a smell, not a pattern.

JWTs: Tokens You Can Peek Into

The most famous Base64 payload on the web is the JSON Web Token, and the least scary one once you know the shape. Per RFC 7519, a compact JWT is three URL-safe Base64 parts separated by dots: a header, a payload, and a signature, each encoded without padding and without line breaks (RFC 7515 is explicit that no extra characters may sneak in). The header and the payload are plain JSON, which is why everyone can read them, and why everyone should understand the next paragraph before touching a token.

Reading the first two parts is five lines of work with the helper from above, and it is a great way to demystify a token:

$token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ';
[$headerPart, $payloadPart] = explode('.', $token);
$header  = json_decode(base64url_decode($headerPart), true);
$payload = json_decode(base64url_decode($payloadPart), true);
var_dump($header);
// array(2) { ["alg"] => string(5) "HS256" ["typ"] => string(3) "JWT" }
var_dump($payload);
// array(3) { ["sub"] => string(10) "1234567890" ["name"] => string(8) "John Doe" ["iat"] => int(1516239022) }

Now the part that matters: the third part is a signature, and the two parts you just decoded are not secret and not authenticated. Anyone with a packet capture can read them, and anyone with a text editor can rewrite them. Trusting the payload before verifying the signature is the classic JWT bug. For production, do not hand-roll that check. The community answer is the firebase/php-jwt package, currently at v7, conforming to RFC 7519 and requiring PHP 8.0 or newer. Install it with Composer:

composer require firebase/php-jwt

Then the API verifies first and hands you the payload only if the signature checks out:

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$secret = 'correct-horse-battery-staple-long-enough-secret';
try {
  $claims = JWT::decode($token, new Key($secret, 'HS256'));
  var_dump($claims->sub); // a property, and only after the signature checked out
} catch (UnexpectedValueException $e) {
  // malformed token, bad signature, or expired claims
}

One version note: v7 of the library enforces minimum key lengths for the HMAC algorithms, so an HS256 secret shorter than 32 bytes is rejected with a DomainException before any Base64 is even touched. Keep your secrets long; the library does not let you forget to.

Watch the order in that API: JWT::decode() throws on a bad signature, an expired token, or a missing algorithm instead of returning garbage, so a payload you receive back is one you can trust. The hand-rolled version above is for understanding, and for peeking at tokens that were not meant for you; the library is for trusting.

HTTP Basic Auth, The Oldest Header

The oldest authentication header on the web still rides on Base64. Per RFC 7617, an HTTP Basic request sends Authorization: Basic followed by the Base64 encoding of username:password. The RFC is explicit that this is encoding, not protection: anyone with a packet capture can decode both halves in one keystroke. Your job on the decode side is to parse the header, decode strictly, and compare with a timing-safe function.

function basic_credentials(string $header): ?array
{
  if (!str_starts_with($header, 'Basic ')) {
    return null;
  }
  $decoded = base64_decode(substr($header, 6), true);
  if ($decoded === false || !str_contains($decoded, ':')) {
    return null;
  }
  [$user, $password] = explode(':', $decoded, 2);
  return [$user, $password];
}
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$creds = basic_credentials($header);
if ($creds !== null
  && hash_equals('alice', $creds[0])
  && hash_equals('secret123', $creds[1])
) {
  // authenticated
}

Two details keep this safe. The limit of 2 in explode() matters because a password may legally contain colons, and the comparison should be hash_equals(), never ==, so an attacker cannot time their way through your user list. And serve this only over HTTPS; on a plain connection the Base64 layer is window dressing.

Email, Where It All Began

Base64 was born for a specific problem: the mail transport only carried 7-bit ASCII, yet people wanted to send binaries. The MIME standard (RFC 2045, section 6.8) made Base64 one of the binary transfer encodings and added two house rules. First, encoded lines must not exceed 76 characters. Second, decoding software must ignore every character outside the alphabet, line breaks included. That second rule is exactly why PHP's decoder, in either mood, chews through a CRLF-wrapped payload without any preprocessing from you. (This is also the origin of the \r\n tolerance you saw in the strict mode table above.)

$png = "\x89PNG\r\n\x1a\n" . random_bytes(256);
$wrapped = chunk_split(base64_encode($png), 76, "\r\n");
// later, on the receiving end, no cleanup needed:
$decoded = base64_decode($wrapped, true);
var_dump($decoded === $png); // bool(true): every byte made the round trip

Two practical notes. First, the wrapping adds weight: with a CRLF every 76 characters, a 100 KB attachment arrives as roughly 137 KB of text (the usual four-thirds factor, plus the line-break overhead). Second, for real-world mail with headers, multiple parts, and quoted-printable siblings, the optional mailparse extension dissects full RFC 822 messages part by part; for a single known attachment, strict decoding is all you need.

PEM Armor: Keys And Certificates

Certificates and keys travel in PEM armor: a BEGIN label, a block of Base64 in 64-character lines, and an END label. The 64-character line length is a convention inherited from the original Privacy Enhanced Mail specification (RFC 1421), and OpenSSL tools expect it, so it matters when you re-armor. When you decode, it does not matter at all: the decoder simply ignores the line breaks.

$pem = file_get_contents('/etc/ssl/my-key.pem');
preg_match('/-----BEGIN ([A-Z ]+)-----\s*(.*?)\s*-----END \1-----/s', $pem, $m);
$label = $m[1];
$der = base64_decode(preg_replace('/\s+/', '', $m[2]), true);
if ($der === false) {
  // not Base64 after all
}
var_dump($label); // string(13) "PRIVATE KEY"

The decoded bytes are DER, a compact binary serialization, and that is what the openssl_* functions ultimately work with. The backreference \1 in the regex is the quiet hero: it guarantees the END label matches the BEGIN label, which is how you avoid stitching a certificate's END onto a key's BEGIN when a file contains several blocks.

Streams And Big Payloads

Decoding is the direction that helps you: the output is three quarters the size of the input, so memory pressure from Base64 is rare. Still, when a multi-hundred-megabyte .b64 file lands on disk, you have two tools for keeping the footprint flat.

The first is chunked decoding. Split the cleaned input into pieces whose length is a multiple of four characters, decode each piece strictly, and concatenate. Every chunk is a self-contained valid payload, so nothing is lost at the borders, and a corrupted file fails fast with an offset you can report.

$clean = str_replace(["\r", "\n"], '', file_get_contents('/var/www/uploads/huge.b64'));
$decoded = '';
$chunkSize = 4 * 50000; // a multiple of four characters, about 150 KB out per call
for ($offset = 0; $offset < strlen($clean); $offset += $chunkSize) {
  $part = base64_decode(substr($clean, $offset, $chunkSize), true);
  if ($part === false) {
    exit('Corrupted payload near offset ' . $offset);
  }
  $decoded .= $part;
}

A megabyte of Base64 decodes in well under a millisecond on modern hardware, so this loop costs almost nothing; pick it for its validation and reporting properties, not for speed.

The second tool is a citizen of the streaming world: the convert.base64-decode stream filter. It works on any PHP stream, so you can decode straight from a file pointer, php://input, or a memory stream without ever holding the whole encoded text in one variable. It also accepts a line-break-chars parameter as a hint for what to strip from the payload:

$in = fopen('/var/www/uploads/huge.b64', 'rb');
$out = fopen('/var/www/uploads/huge.bin', 'wb');
stream_filter_append($in, 'convert.base64-decode', STREAM_FILTER_READ);
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);

Which tool do you pick? The filter when the data flows through a stream and you want PHP to handle the plumbing; the chunk loop when you need per-chunk validation, progress reporting, or the offset of the corruption.

Databases, Config Files And Environment Variables

Base64 is a text container, which is why it shows up in places you would not expect. In databases, a binary blob (a file, an icon, a serialized structure) can live in a TEXT column as Base64, surviving every tool that assumes text. Expect the stored value to be about 33 percent larger than the original, and size your columns accordingly. In config files and environment variables, Base64 is the trick for smuggling values that would otherwise break the format: a database DSN with semicolons, a password with quotes, a value with a newline.

// .env or config, written by the ops person:
//   DB_DSN_B64 = cGc6aG9zdD1kYjtwYXNzd29yZD1xdSJvdGU=
$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
  exit('DB_DSN_B64 is not valid Base64.');
}
// $dsn is now: pg:host=db;password=qu"ote

The same caution applies twice here. First, this is format safety, not secrecy: the moment a developer reads the config file, they can decode the value in one call. Never store a secret as Base64 and call it encrypted. Second, validate at boot: a corrupted or half-pasted env value is a false from the strict call, and a one-line check turns a cryptic runtime error into an actionable startup message.

From The Command Line

Not all decoding happens inside a web request. CLI scripts, cron jobs, and one-liners decode Base64 all the time, and the command line is where the function meets php://stdin:

php -r 'fwrite(STDOUT, base64_decode(file_get_contents("php://stdin"), true));' < payload.b64 > restored.bin

The shell already has its own Base64 utility (coreutils base64 -d), and it is fine for quick work; the PHP one-liner is for when the next step is PHP logic: writing to a database, calling an API, running a validation. Two shell-specific gotchas. The output of a decode is raw bytes, so send it to a file or to a command that understands bytes, not to a terminal that will mangle them. And keep the strict flag on in the one-liner, because a truncated paste in a terminal deserves a false, not three garbage bytes.

Pitfalls With A PHP Accent

A quick tour of the traps that are specific to PHP, collected in one place:

  • The lenient default is the big one. base64_decode('V@hpcy') returns three bytes of garbage with no warning, so every decoder of untrusted input needs the strict flag and a false check.
  • A single character decodes to an empty string in lenient mode, and so does a string of only spaces. An empty result proves almost nothing; only false means failure, and you only get it in strict mode.
  • The + in a query string is already a space before PHP sees it. If a client sends ?token=abc+def without percent-encoding it, PHP hands you abc def (that is form-encoding behavior, shared by parse_str() and urldecode()), and no amount of decoding magic brings the plus back. URL-safe Base64 (no plus at all) is the fix for tokens in URLs.
  • Missing padding is filled in for you, silently. Seven characters decode like eight; that is convenient, but it also means PHP will accept the odd Zm9vYmF= grouping that other platforms' strict decoders reject.
  • The ghost of mbstring.func_overload. The long-deprecated setting that rewrote strlen() and friends to count characters (removed in PHP 8.0) used to break Base64 byte math on UTF-8 strings. Legacy code you inherit may still carry comments and workarounds for it. Delete them.
  • Decoded bytes are not a UTF-8 string. Running preg_match() with the /u flag or mb_substr() on decoded binary is an instant source of "malformed input" errors. Sniff first, then decide.
  • Passing null is deprecated since PHP 8.1. If a variable may be null, coalesce it to '' before the call.
  • $_GET and friends are decoded with form rules, not URL rules. If a value arrived percent-encoded, rawurldecode() is the safer inverse, because it leaves + alone.

A Short History Of base64_decode

Base64 itself is older than the web (the standard governing it, RFC 4648, dates from 2006, and it codified the MIME encoding from 1996, which itself descends from the PEM armor of the early 1990s). The PHP story is its own little changelog.

PHP 4 shipped base64_decode() as a core function with no options and no strict mode; the lenient mood was the only mood, and there was no way to ask the decoder to complain. PHP 5.2.0, in November 2006, added the $strict flag, and the changelog entry is worth reading: it was added to enforce RFC 3548 compliance, the predecessor of today's RFC 4648. That one flag turned out to be the most useful addition in the function's life.

Then came the debugging years. PHP 5.3 and 5.4 fixed a run of strict-mode bugs: leading padding handled improperly, whitespace after padding rejected, a crash on some invalid inputs, and later an integer overflow on pathological strings. Each fix tightened the behavior you see in the table above. PHP 8.0 gave both Base64 functions native parameter and return types, the signature you saw at the top of this article, and the same release line removed mbstring.func_overload, the setting that had quietly broken byte math for years. PHP 8.1 deprecated passing null to them. Since then, the surface has been frozen: one parameter, one flag, one return type, unchanged.

A Few Nerd Delights

Because this is a long-form reference, here are some PHP-specific facts that are simply fun:

  • The empty identity. base64_encode('') and base64_decode('') are both ''. The functions treat emptiness as a first-class value in both directions, with no false involved.
  • A strange address. In the PHP manual, both Base64 functions live in the "URL Functions" chapter of the "Other Basic Extensions" book. There is no dedicated "encoding" chapter; that is where you will find them, right between parse_url() and its friends.
  • The decoder is a homomorphism. A classic php.net user note observes that the function is a homomorphism between modulo-4 and modulo-3 segmented strings, which is the formal way of saying any multiple-of-four split is a valid split. That is why the chunked decoding section works at all, and why a 1 MB file can be decoded in 50 KB slices with zero loss.
  • One parameter, one flag. In more than twenty years, base64_decode() gained exactly one parameter ($strict) and base64_encode() gained none.
  • It has older siblings. The same core chapter also carries convert_uuencode() and convert_uudecode(), the relics of the dial-up era when uuencode was the binary transport of choice. You will almost never need them, but if an ancient .uu file ever lands in your inbox, PHP can open it.
  • Strict mode keeps an open door for email. CRLF and the other four whitespace characters sail through strict mode on purpose, so a MIME-wrapped attachment needs no preprocessing. Everything else, NUL bytes included, is a false.

The Other Direction

That is the decoder side, and it is where most of the pain lives, because decoding is where you meet other people's data: their padding choices, their line breaks, their charsets, their tokens. The other direction, turning bytes into a Base64 string with base64_encode(), is a calmer animal: it never fails, it has no strict mode, and its own set of traps (double-encoding, wrapping mismatches, the size bill) get their own guide. Base64 encoding in PHP, linked from this page, covers the encoder in the same depth.

Last updated: 2026-08-29

Related article: Base64 Encoding in PHP: A Complete Guide