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 Encoding in PHP: A Complete Guide

You have data that must survive a channel that does not like it. A binary blob that needs to sit in a JSON field. An image that must live inside an HTML tag. A certificate that belongs in a config file. A token that will travel through URLs, headers, and cookies. This is the daily life of Base64 encoding: it rewrites every three bytes of raw data as four characters from a 64-letter alphabet, with one or two = signs finishing the tail, so the result is plain text that anything can carry. The main page of this site explains the format in full, so this article focuses on what PHP gives you, what it quietly decides for you, and where the traps are.

On the PHP side, the story starts with good news: base64_encode() has lived in the core since PHP 4, it takes one argument, it always returns a string, and it cannot fail. There is no strict mode, no error path, no configuration. The encoding is deterministic: the same bytes always produce the same letters. Your job as the developer is not to make the function work, it is to make the surrounding world behave: pick the right alphabet for the destination, add the right line breaks, convert the right charset, and carry the 33 percent size bill with open eyes. (Base64 normally expands data by about a third, four characters per three input bytes; keep that in the back of your mind, because it keeps coming back.)

By the end of this article you will know how to produce every flavor of Base64 a PHP developer actually meets: single-line output, MIME-wrapped email, PEM-wrapped keys, URL-safe tokens, and data URIs, plus the streaming tricks for when the data is too big to hold in memory.

One Function, Zero Options

The entire API, exactly as modern PHP reports it:

base64_encode(string $string): string

Read that again. One parameter, one return value, no flags. The manual describes it as MIME base64, "designed to make binary data survive transport through transport layers that are not 8-bit clean, such as mail bodies". Note what that wording does not promise: no line breaks, no wrapping, no opinion about where the output will live. The function emits one long line, and whatever wrapping the destination wants is your job with a second call. Since PHP 8.0 the signature carries native types, and since PHP 8.1 passing null raises a deprecation notice, so coalesce any nullable value to '' first.

The output size follows a fixed pattern you can predict before calling:

Input bytes Output characters Padding
0 0 none
1 4 two =
2 4 one =
3 4 none
3,000,000 4,000,000 none
100,000 133,336 none

The pattern is four characters for every complete group of three bytes, plus a final partial group padded with one or two = signs. One consequence worth knowing: one byte and three bytes both produce four characters, so the encoded length hides the exact input size. You can estimate it (divide by four, multiply by three, subtract the pads) but you cannot read it off exactly.

Where The Line Breaks Go

Since base64_encode() never wraps on its own, the wrapping decision is a destination problem. In practice there are three answers.

No line breaks. The raw function output, exactly one line. This is what you want for URLs, JSON payloads, headers, database values, and anything else where a line break would be a bug. This is also what most people mean by "just give me the Base64".

MIME wrapping: 76 characters plus CRLF. The email convention from RFC 2045, section 6.8: encoded lines must not exceed 76 characters, and decoders must ignore the line breaks. The classic companion call is chunk_split(), which the manual pairs with base64_encode() in its "See Also" list for exactly this reason:

$binary = file_get_contents('/var/www/uploads/report.pdf');
$wrapped = chunk_split(base64_encode($binary), 76, "\r\n");

PEM wrapping: 64 characters plus LF. Keys and certificates use the older Privacy Enhanced Mail convention (RFC 1421): shorter 64-character lines. Same tool, different numbers:

$der = file_get_contents('/etc/ssl/raw-key.der');
$armor = "-----BEGIN PRIVATE KEY-----\n"
  . chunk_split(base64_encode($der), 64, "\n")
  . "-----END PRIVATE KEY-----\n";

One chunk_split() gotcha applies to all three: the function appends the separator at the end of the result even when the input length is an exact multiple of the line length. If a downstream consumer trips over a trailing empty line, that is why, and an rtrim() of the separator fixes it. Also note the asymmetry that will save you someday: decoders ignore line breaks entirely, so a MIME-wrapped payload and an unwrapped one decode to the same bytes. Wrapping is a courtesy to line-based tools and humans, not a semantic difference.

Making The Output URL-Safe

The standard alphabet includes + and /, and both are trouble outside a text file. A + in a form-encoded query string becomes a space before your application ever sees it, and / is a path separator in URLs. Filenames and tokens have their own complaints. RFC 4648, section 5, solves this with the URL and filename safe alphabet: + becomes -, / becomes _, and the trailing = padding is usually dropped. The RFC insists this "should not be regarded as the same as the base64 encoding", so treat it as a distinct format, commonly called base64url.

Producing it is two string operations:

function base64url_encode(string $data): string
{
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
var_dump(base64url_encode("hi>?there\x00bin")); // string(18) "aGk_PnRoZXJlAGJpbg"

When to use it: JSON Web Token parts, OAuth state and nonce parameters, API ids you put in URL paths, and anything that will be copied into an address bar or a file name. When not to use it: email bodies, PEM armor, and any place a standard-alphabet consumer is on the other end, because - and _ are not in their vocabulary. And do not mix the two alphabets silently: a token encoded URL-safe must be decoded URL-safe, everywhere, forever. That is the whole interoperability rule of base64url.

Unicode And The Bytes You Meant

PHP strings are byte sequences, and base64_encode() will encode whatever bytes it is given without asking what they mean. That is a feature until the day your "text" is not actually the encoding you think it is. The classic failure: a string that looks like UTF-8 in your editor but arrived from a legacy source as Windows-1252. Encode those bytes as-is and the receiver, who will decode and assume UTF-8, gets mojibake instead of your accented letters.

The fix is to normalize before you encode, with the mbstring extension (bundled with standard PHP builds):

$fromLegacy = "caf\xE9 au lait"; // Windows-1252 bytes: the é is 0xE9
$utf8 = mb_convert_encoding($fromLegacy, 'UTF-8', 'Windows-1252');
$encoded = base64_encode($utf8);
var_dump($utf8); // string(13) "café au lait": the é is now two UTF-8 bytes

If the source is already UTF-8, you can skip the conversion, and a cheap sanity check is mb_check_encoding($utf8, 'UTF-8'). One sentence of advice: never try to "fix" an already-encoded Base64 string by re-encoding it as text. That is the double-encoding trap from the pitfalls section below, and it is the single most common Base64 bug in PHP codebases.

Files, Blobs And The .b64 Convention

The most straightforward encoding job: a file becomes text. PHP strings are bytes, so there is no "binary mode" to worry about; file_get_contents() hands you the exact bytes and base64_encode() hands you the exact text:

$binary = file_get_contents('/var/www/uploads/photo.png');
$encoded = base64_encode($binary);
file_put_contents('/var/www/uploads/photo.png.b64', $encoded);
var_dump(strlen($binary), strlen($encoded)); // the 33% bill, every time

Two habits keep this safe. First, know what you are encoding. The finfo class (fileinfo extension, bundled with standard PHP builds) tells you the real type from the bytes, not the file name:

$mime = (new finfo(FILEINFO_MIME_TYPE))->file('/var/www/uploads/photo.png');
var_dump($mime); // string(9) "image/png"

Second, remember the size bill when you plan storage: a 500 KB image becomes a 670 KB text file, and a 1 GB video becomes a 1.33 GB text file. That is why the large-data section below exists.

Data URIs: Putting An Image In The Page

A data URI embeds the payload directly in the URL, so no second request is needed to fetch it. RFC 2397 defines the shape: data:, an optional media type, an optional ;base64 flag, a comma, and the data. For binary media like images the flag is present, so the payload is exactly what base64_encode() produced:

function data_uri_for(string $path): string
{
  $mime = (new finfo(FILEINFO_MIME_TYPE))->file($path);
  $payload = base64_encode(file_get_contents($path));
  return 'data:' . $mime . ';base64,' . $payload;
}
$uri = data_uri_for('/var/www/uploads/avatar.png');
echo '<img src="' . $uri . '" alt="avatar" />' . "\n";

Why Base64 here? Because a URI cannot safely contain raw bytes or commas, and the Base64 alphabet needs no escaping at all. The trade-offs are real, though. The encoded payload is about 33 percent bigger than the file, which makes the HTML document itself bigger. Browsers will not cache a data URI the way they cache a file URL, so every page view re-downloads the bytes. And the RFC itself says data URIs are only useful for short values; old HTML parsers had hard limits on attribute length, and modern browsers, while far more generous, still do not enjoy megabytes inside a tag. Use them for avatars, icons, and small inline graphics; use real files for everything else.

JWTs And API Tokens

JSON Web Tokens are the flagship consumer of Base64 in modern APIs, and they use the URL-safe, unpadded dialect from the section above. Per RFC 7519, a compact JWT is three dot-separated base64url parts: header, payload, signature. The header and payload are plain JSON; the signature is raw bytes. Building one by hand is a pleasant way to see every moving part:

function base64url_encode(string $data): string
{
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode([
  'sub' => '1234567890',
  'name' => 'John Doe',
  'iat' => 1516239022,
]));
$signingInput = $header . '.' . $payload;
$signature = base64url_encode(hash_hmac('sha256', $signingInput, $secret, true));
$token = $signingInput . '.' . $signature;

Two things to notice. The signature is Base64url encoding of raw HMAC bytes, which is why hash_hmac() is called with true for raw output. And the header and payload are readable by anyone, which is by design: a JWT is a signed ticket, not a secret. For production, you do not hand-roll signing or verification. The community package is firebase/php-jwt (v7, requires PHP 8.0 or newer), installed with Composer:

composer require firebase/php-jwt
use Firebase\JWT\JWT;
$secret = 'correct-horse-battery-staple-long-enough-secret';
$token = JWT::encode([
  'sub' => '1234567890',
  'name' => 'John Doe',
  'exp' => time() + 3600,
], $secret, 'HS256');
var_dump(substr_count($token, '.')); // int(2): header, payload, signature

Version note for the v7 line of the library: the HMAC algorithms enforce a minimum key length, so an HS256 secret shorter than 32 bytes is rejected before any encoding happens. Long secrets are the norm anyway; this just makes the library refuse to be casual about it.

The library handles the base64url conversion, the signing, and the expiry checks for you, and it throws typed exceptions instead of returning half-trusted data. When you write tokens with it, you never touch base64_encode() directly, which is exactly as it should be.

HTTP: Basic Auth And The WebSocket Handshake

Two header-building jobs where PHP does the Base64 and the protocol does the rest.

HTTP Basic auth (RFC 7617): the client sends Authorization: Basic plus the Base64 of username:password. Building it is one string concatenation:

function basic_authorization_header(string $username, string $password): string
{
  return 'Basic ' . base64_encode($username . ':' . $password);
}
$headers[] = 'Authorization: ' . basic_authorization_header('alice', 'secret123');

Say it out loud once, because the RFC makes you: this is encoding, not protection. Anyone with a packet capture recovers both halves in one keystroke, so Basic auth belongs on HTTPS connections only.

The WebSocket handshake (RFC 6455): the server proves it heard the client by echoing a transformed key. It concatenates the client's Sec-WebSocket-Key with a fixed magic GUID, takes SHA-1 of the result, and Base64-encodes the digest. This is standard Base64, padding included, because it lives in a header, not a URL:

function websocket_accept_key(string $clientKey): string
{
  return base64_encode(hash('sha1', $clientKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}
$accept = websocket_accept_key('dGhlIHNhbXBsZSBub25jZQ==');
var_dump($accept); // string(28) "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

That example is the one from the RFC itself, which makes it a handy self-test: if your implementation produces the same 28 characters, the WebSocket layer is speaking correctly.

Email: The Original Use Case

Everything else in this article is a descendant of one fact: SMTP was designed to carry 7-bit ASCII, and people wanted to send binaries. The MIME standard's answer, in RFC 2045 section 6.8, was Base64 as a Content-Transfer-Encoding, with the two house rules you have already met: lines of at most 76 characters, and decoders that ignore every character outside the alphabet. So a PDF attachment travels like this:

$pdf = file_get_contents('/var/www/uploads/report.pdf');
$attachment = chunk_split(base64_encode($pdf), 76, "\r\n");
// the mail library now puts $attachment in the MIME part,
// with Content-Transfer-Encoding: base64

The practical numbers: the encoding itself costs 33 percent, and the CRLF every 76 characters costs a bit more, so a 100 KB attachment ships as roughly 137 KB of text. When you write email from PHP, the libraries (PHPMailer and its stable relatives) do the wrapping for you, and you hand them the raw binary. If you ever see a 76-character-wide wall of letters in a raw .eml file, now you know the exact algorithm that produced it.

PEM Armor For Keys And Certificates

Keys and certificates need more than a wall of letters; they need labels. PEM armor is a BEGIN line, a 64-character-wrapped Base64 block, and an END line, a convention inherited from Privacy Enhanced Mail (RFC 1421) and kept alive by OpenSSL. PHP's openssl extension produces and consumes this shape directly:

$res = openssl_pkey_new([
  'private_key_bits' => 2048,
  'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
openssl_pkey_export($res, $pem);
// $pem is already armored: BEGIN label, 64-char lines, END label

The interesting case is when the armor must be rebuilt by hand, for example when you receive raw DER bytes from an API and need a PEM file for a tool that only reads PEM. The convention is 64 characters per line, LF line breaks, and a label that names the contents:

$rearmored = "-----BEGIN CERTIFICATE-----\n"
  . chunk_split(base64_encode($der), 64, "\n")
  . "-----END CERTIFICATE-----\n";
var_dump(openssl_x509_parse($rearmored) !== false); // bool(true): the armor is valid

Get the label wrong and the file is garbage, no matter how perfect the Base64 is. And get the line length wrong and most tools will still read it, because decoders ignore line breaks, but diff tools and humans will suffer. Sixty-four is the number.

Config Files, Environment Variables And Databases

Base64 is a text container, which makes it a smuggling tool for values that would otherwise break their container. A database DSN full of semicolons and quotes, a JWT in a .env file, a binary blob in a TEXT column: all of them become one long safe string.

The environment variable flavor is a two-step ritual. Once, on the machine that builds the config, you encode:

echo 'DB_DSN_B64=' . base64_encode('pg:host=db;password=qu"ote') . PHP_EOL;

Then, on every boot of the application, you decode and validate at startup, so a half-pasted config fails loudly instead of cryptically:

$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
  exit('DB_DSN_B64 is not valid Base64.');
}

For databases, the same idea stores binary in text columns. The size bill applies: the stored value is about 33 percent larger than the blob, so a 1 MB file occupies roughly 1.33 MB in the column, and you should pick the column type with that in mind. And the same warning as everywhere else: this is format safety, not secrecy. Anyone who can read the config or query the column can reverse it in one call. If the value is sensitive, encrypt it; Base64 only makes it portable.

Large Data And Steady Memory

Encoding is the direction that costs you: the output is a third bigger than the input, so a 2 GB binary wants 2.66 GB of encoded string in memory. On a long-lived web process or a memory-limited host, that is a reason to stream instead of slurp, and PHP gives you two ways.

The first way is the convert.base64-encode stream filter, the streaming twin of the function. It supports parameters as an associative array: line-length for the wrap width and line-break-chars for the separator, which reproduces the chunk_split() effect without holding the whole string:

$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
stream_filter_append($out, 'convert.base64-encode', STREAM_FILTER_WRITE, [
  'line-length' => 64,
  'line-break-chars' => "\n",
]);
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);

The second way is the classic 57-byte trick, and it is a small piece of PHP lore. A 76-character MIME line holds exactly 57 bytes of original data, so if you read the input file in chunks of a multiple of 57 bytes, every chunk encodes independently, with no leftover bits to carry between chunks. Reading in 8151-byte chunks (57 times 143, which is a 76-line page of output close to PHP's default 8192-byte I/O buffer) keeps memory flat while the file streams out MIME-perfect:

$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
while (!feof($in)) {
  $plain = fread($in, 57 * 143);
  $encoded = chunk_split(base64_encode($plain), 76, "\r\n");
  fwrite($out, $encoded);
}
fclose($in);
fclose($out);

Which one to pick? The filter when you want PHP to own the plumbing and you do not care about the exact chunk boundaries; the 57-byte loop when you want deterministic MIME output, progress hooks, or a hard cap on buffer size. Either way, the memory footprint stays at one chunk, not one file.

Pitfalls With A PHP Accent

The traps that show up in real PHP codebases, collected in one place:

  • Double encoding. The classic: a value that is already Base64 (from an env var, a database, a previous script) gets passed through base64_encode() again because nobody checked. The result decodes once and yields... more Base64. The cure is a round-trip check at the boundary, or a single well-known function that owns all encoding in the codebase.
  • The + in URLs. Standard output contains + and /. In a form-encoded query string the plus becomes a space before your code sees it; in a path it is a separator. For anything URL-bound, emit base64url or percent-encode the whole value with rawurlencode().
  • The trailing separator. chunk_split() ends its result with the separator even on exact multiples of the line length. A trailing empty line is usually harmless (decoders ignore it) but it trips naive line counters and diff tools. rtrim() the separator if the consumer is picky.
  • Wrap mismatch. Writing with 76-character MIME lines and having a consumer expect 64-character PEM lines (or vice versa) is not a decoding problem, because decoders ignore breaks, but it is a line-tooling and human-reading problem. Pick the convention your destination expects and stick to it.
  • The trailing newline is data. base64_encode() encodes every byte, including a newline at the end of a text file. When two systems produce "different" Base64 for the same-looking text, a trailing \n is the usual suspect.
  • Base64 is not encryption. Encoding a password before it hits the database does not protect it; it formats it. The "encrypted" column is one function call from plaintext for anyone with query access. Encrypt or hash real secrets; Base64 is a transport costume.
  • Memory is a third bigger. On a 32-bit PHP build or a host with tight memory limits, encoding a large binary can fail outright. Stream it, as shown above, before you tune memory_limit.
  • No line breaks are added, ever. "MIME base64" in the function description does not mean "MIME-wrapped output". If your output needs 76-character lines, you add them with chunk_split() or the filter.

A Short History Of base64_encode

The encoding side of the PHP story is almost refreshingly boring, in the best way. base64_encode() arrived in PHP 4 as a core function with one parameter and no options, and it has not gained a single one since. No strict mode was ever needed (there is nothing to be strict about when you are the one producing the data), no padding option was ever added, and the wrapping job was delegated to chunk_split() from day one, which is why the two functions still sit together in the manual's "See Also" lists.

The manual has carried the 33 percent figure for as long as the function has been documented: "Base64-encoded data takes about 33% more space than the original data". That sentence is still there today, and it is the reason the number appears in this article at all. The stream filter convert.base64-encode joined later, and it had its own bug to grow up out of: in 2011, PHP fixed a defect (bug #68532) where the filter omitted padding bytes at chunk boundaries, which is exactly the kind of silent corruption the function-based path never suffers from. PHP 8.0 added the native string parameter and return types, and that is where the changelog ends. One function, one parameter, twenty years, zero options: a monument to getting the surface right the first time.

Fun PHP Facts

Because a reference is not complete without the oddities:

  • The empty identity. base64_encode('') is ''. No padding, no output, no surprises: emptiness in, emptiness out.
  • A strange address. The PHP manual files base64_encode() under "URL Functions" in the "Other Basic Extensions" book. There is no "encoding" chapter; that is where you will find it, alongside parse_url().
  • The alphabet never moved. The same 64 characters have come out of PHP's encoder since PHP 4. A Base64 string produced by a PHP 4 script on a Windows box in 2001 decodes identically on PHP 8.4 on Linux today. That is interoperability with a 25-year track record.
  • One byte and three bytes look identical in length. Both produce four characters; only the padding tells them apart. That is why the size table in this article exists.
  • Fifty-seven is a magic number. A 76-character MIME line holds exactly 57 bytes of original data, and that coincidence is what makes the streaming chunk loop in the large-data section possible without carrying state between reads.
  • It has a dial-up sibling. The same core chapter carries convert_uuencode(), the PHP wrapper for uuencode, the format that encoded binaries for email before MIME standardized Base64. You will almost never need it, but it is the fossil record of this function's original purpose, sitting right next to it.
  • Old browsers were picky about the padding. A php.net note from 2004 reports that Internet Explorer refused cookie names containing =, which is why veteran code sometimes strips the trailing pads from Base64 stored in cookies. Modern setups do not need that trick, but it explains odd rtrim($x, '=') calls you may inherit.

The Flip Side

That is the encoding side, and it is the easier of the two: the function cannot fail, the output is deterministic, and the format is one you control end to end. The harder direction is the one where you receive other people's Base64: their padding choices, their line breaks, their URL-safe dialects, their corrupted pastes. That is where a decoder needs strict mode, a validation pipeline, and a healthy suspicion of everything. Base64 eecoding in PHP, linked from this page, covers the decoding side in the same depth.

Last updated: 2026-08-29

Related article: Base64 Decoding in PHP: A Complete Guide