Base64 Encoding and Decoding: How It Works and When to Use It
Learn how Base64 turns three bytes into four characters, why it adds 33% size, how base64url differs, and what it is not: encryption, compression or security.

You have probably seen a string like SGVsbG8sIFdvcmxkIQ== and wondered what it was. It shows up in email source, in the middle of a login token, in an HTTP header, in a CSS file that embeds an icon, and in a config file that somehow contains a certificate. It is Base64, and it is one of the most widely used encodings on the internet. It is also one of the most misunderstood. People call it encryption, they call it compression, and they paste passwords into it and feel safe. It is neither of those things.
This guide is for developers who use Base64 every week without having looked inside it. You will learn what problem it solves, how the algorithm turns three bytes into four characters, why the output is always about a third larger than the input, how the URL-safe variant differs, and where the format helps or hurts. Every code sample below was run before it was written down, and the outputs are real. When you only need a quick conversion, the DevUtilX Base64 Encoder and Base64 Decoder do the job in your browser.
What Base64 is and why it exists
Base64 is a way to represent arbitrary binary data using only 64 printable ASCII characters. It is a binary-to-text encoding. You give it bytes, and it gives you text that is safe to put almost anywhere text is allowed.
The reason it exists is that many systems were built to carry text, not bytes. Early email protocols assumed 7-bit ASCII lines of limited length. A raw JPEG contains byte values that mean something to those systems: line endings, control characters, values above 127. Send those bytes through a gateway that rewrites line endings or strips the high bit, and the file arrives corrupted. The same problem appears today in smaller forms. JSON has no binary type. A URL cannot contain arbitrary bytes. An HTTP header value cannot contain a raw newline. An environment variable cannot hold a null byte.
Base64 solves this with a deal. It agrees to make the data bigger, and in return it uses only characters that every text channel handles correctly. The alphabet contains uppercase letters, lowercase letters, digits, and two extra symbols, plus an equals sign used for padding. None of those characters are control characters, none are line breaks, and none change meaning when a system converts between character sets.
Notice what Base64 does not promise. It does not hide anything, it does not shrink anything, and it does not verify anything. It is a transport costume for bytes. Anyone can take the costume off, and no key is needed.
A short history
The idea of encoding bytes as printable text is older than the web. Before email supported attachments, people used tools such as uuencode to turn binary files into text for mailing lists and newsgroups. Several incompatible schemes appeared, each with a slightly different alphabet and line format.
The MIME standards for email, starting with RFC 2045, gave Base64 a formal definition as a content transfer encoding. That is why Base64 is so tightly tied to email in developers' minds, and why the MIME flavour has line breaks every 76 characters. Later, RFC 4648 collected the Base16, Base32 and Base64 encodings into one specification, defined the standard alphabet precisely, and added the URL- and filename-safe alphabet. Most modern code follows RFC 4648. The Wikipedia article on Base64 lists the many variants that exist in the wild, which is a useful reminder that "Base64" is a family, not a single format.
How the algorithm works
The core idea fits in one sentence: take three bytes, which are 24 bits, and split them into four groups of six bits. Each six-bit group is a number from 0 to 63, and each number maps to one character in a 64-character alphabet.
Why three and four? Because 3 times 8 equals 24, and 4 times 6 equals 24. Three bytes divide evenly into four six-bit groups, so no bits are left over and none are wasted. Six bits give exactly 64 values, which is where the name comes from.
The alphabet
The standard alphabet from RFC 4648 is ordered like this:
| Index range | Characters | Count |
|---|---|---|
| 0 to 25 | A to Z |
26 |
| 26 to 51 | a to z |
26 |
| 52 to 61 | 0 to 9 |
10 |
| 62 | + |
1 |
| 63 | / |
1 |
The order matters. Index 0 is A, index 26 is a, index 52 is 0. If you ever see an output where lowercase letters come before uppercase, you are looking at a different scheme, not RFC 4648 Base64.
A fully worked example: encoding "Man"
The classic example is the three-letter word Man. Follow each step.
- Find the byte values. In ASCII and UTF-8,
Mis 77,ais 97, andnis 110. - Write them in binary. 77 is
01001101, 97 is01100001, 110 is01101110. - Join them into one 24-bit string.
010011010110000101101110. - Split into four groups of six bits.
010011,010110,000101,101110. - Convert each group to a number. They are 19, 22, 5 and 46.
- Look up each number in the alphabet. 19 is
T, 22 isW, 5 isF, 46 isu.
The result is TWFu. You can confirm it in any runtime: btoa("Man") returns TWFu. Three bytes went in, four characters came out.
What happens when the length is not a multiple of three
Real data rarely has a length divisible by three. Base64 handles the leftover bytes with padding. The encoder works on the final partial group as if it were followed by zero bits, then marks how many bytes were missing with equals signs.
Take the two-byte input Ma. The bits are 01001101 01100001, which is 16 bits. Six-bit groups need 18 bits, so the encoder appends two zero bits: 010011 010110 0001 followed by 00. That gives the groups 19, 22 and 4, which are T, W and E. Only three characters carry data, so the encoder adds one = to reach a full block of four. The result is TWE=.
Now take the single byte M. Its bits are 01001101, which is 8 bits. Six bits make the first group, 010011, and the remaining two bits 01 are padded with four zeros to form 010000. The groups are 19 and 16, which are T and Q. Two characters carry data, so the encoder adds two equals signs. The result is TQ==.
We ran all three through btoa and they match: TWFu, TWE= and TQ==. The pattern to remember:
| Input bytes in final group | Data characters | Padding |
|---|---|---|
| 3 | 4 | none |
| 2 | 3 | = |
| 1 | 2 | == |
Padding exists so that the decoder always sees a length that is a multiple of four, and so that two encoded strings can be concatenated at block boundaries. It carries no data of its own. As you will see, many modern formats drop it.
Decoding is the same steps backwards
To decode, you map each character back to its six-bit value, join all the bits, and cut them into bytes of eight. For TWE=, the decoder reads T as 19, W as 22, E as 4, and ignores the padding character. That gives the bits 010011 010110 000100, which is 18 bits. The decoder takes the first 16 bits as two bytes, 01001101 and 01100001, which are M and a, and discards the final two bits because they are leftover padding zeros.
This is also why a string with the wrong length cannot decode cleanly. A single data character provides only six bits, which is not enough to make even one byte, so a length of 4n plus one is never valid.
Why the output is about 33 percent larger
Every three input bytes become four output characters, and each character is one byte in ASCII. So the output is 4/3 the size of the input, an increase of 33.33 percent, rounded up to a multiple of four.
The exact formula for the encoded length with padding is 4 * ceil(n / 3). Here is that formula checked against real output, with the overhead computed from it:
| Input bytes | Encoded characters | Overhead |
|---|---|---|
| 1 | 4 | 300.00% |
| 2 | 4 | 100.00% |
| 3 | 4 | 33.33% |
| 10 | 16 | 60.00% |
| 100 | 136 | 36.00% |
| 1,000 | 1,336 | 33.60% |
| 1,000,000 | 1,333,336 | 33.33% |
Tiny inputs pay a large relative penalty because of padding, but they are small in absolute terms. For anything large, plan for one third more bytes. If you add MIME line breaks, which add two bytes (a carriage return and line feed) for every 76 characters, the overhead rises slightly above that, to about 35 percent.
Standard, URL-safe and other variants
The two symbols at the end of the standard alphabet, + and /, cause trouble in places where those characters already have a job. A + in a URL query string can be read as a space. A / in a URL path is a separator. A / in a filename is a directory boundary. RFC 4648 defines a second alphabet, often called base64url, to fix this.
base64url
The URL- and filename-safe alphabet replaces + with - and / with _. Everything else is identical. Here is the difference on real bytes, from Node.js:
const bytes = Buffer.from([0xfb, 0xff, 0xfe]);
console.log(bytes.toString("base64")); // +//+
console.log(bytes.toString("base64url")); // -__-
console.log(Buffer.from("hi?>~").toString("base64")); // aGk/Pn4=
console.log(Buffer.from("hi?>~").toString("base64url")); // aGk_Pn4
The first pair shows all four characters changing. The second pair shows the / becoming _ and, in Node's base64url output, the trailing = disappearing.
Padding is optional in base64url usage
RFC 4648 says padding must be included unless the specification that uses the encoding says otherwise. Many specifications do say otherwise. JSON Web Signature, the format behind JWT, uses base64url without padding. Node's base64url encoding omits it, while Python's urlsafe_b64encode keeps it. This difference is a common source of bugs when two systems exchange tokens, and a decoder should tolerate both. We cover the repair in the troubleshooting section.
MIME line wrapping
In MIME email, Base64 output is broken into lines of at most 76 characters, each ending with a carriage return and line feed. Decoders must ignore those breaks. Python's base64.encodebytes follows this convention and wraps at 76 characters, with a newline after every line including the last. For 60 x bytes, it returns:
b'eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4\neHh4\n'
The first line is 76 characters long and the second is the remaining four. If you need a single unbroken string, as you do for a JSON field or a data URI, use b64encode instead. Some tools, including the base64 command on certain systems, wrap output by default, which surprises people who copy the result into a config file and find a newline in the middle.
Other alphabets you will meet
There are further variants. Some systems use a different pair of symbols, some use a different character order, and the old crypt-style hash encodings use another ordering. They are all "Base64" in spirit but not interchangeable. If a decoder produces garbage for an input that looks like Base64, check which alphabet the producer used before assuming the data is damaged.
Real-world use cases
Base64 appears wherever binary has to ride inside text. These are the places you will most often meet it.
Email attachments
Every attachment in an email is carried inside a MIME part with a Content-Transfer-Encoding: base64 header, and the body of the part is the wrapped Base64 text. Mail clients encode and decode automatically, so you rarely see it, but opening the raw source of any message with an attachment reveals pages of it. The 33 percent growth is the reason a 10 MB attachment can be rejected by a server with a 10 MB limit: the message is about 13.3 MB on the wire.
JWT segments
A JSON Web Token has three parts separated by dots: a header, a payload and a signature. The header and payload are JSON objects encoded with base64url. Here is a header and payload encoded in Node.js:
const h = Buffer.from('{"alg":"HS256","typ":"JWT"}').toString("base64url");
const p = Buffer.from('{"sub":"42","name":"Ada"}').toString("base64url");
console.log(h + "." + p);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ
Notice that both segments start with eyJ. That is not a coincidence. JSON objects begin with {", and those two bytes always encode to eyJ when followed by another byte. Once you recognise it, you can spot a JWT in logs at a glance. The important lesson is that the payload is only encoded, not protected. Anyone who holds the token can paste the middle segment into a decoder and read the claims. The signature prevents tampering, but it does not provide secrecy. Never place anything in a JWT payload that the holder should not see.
The Basic authentication header
HTTP Basic authentication sends username:password as Base64 in the Authorization header. For the credentials aladdin:opensesame, the header is:
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
That output is exactly what Node produced for the input. Base64 here exists only so the colon-separated pair survives as a single header token, regardless of which characters the password contains. It provides no confidentiality at all, which is why Basic authentication must only travel over HTTPS.
Embedding binary in JSON and XML
JSON has strings, numbers, booleans, arrays, objects and null. It has no bytes type, so an API that must return an image thumbnail, a signature, or a protobuf blob inside a JSON document usually uses a Base64 string. The same holds for XML. This is convenient, since the whole record travels as one document, but it carries the 33 percent cost, and the compression caveats we discuss below. Our guide to JSON minification and payload size explains why Base64 blobs are among the largest and least compressible parts of a response, and why serving large files separately and returning a URL is often the better design. For readable structure in the surrounding document, see the JSON formatting guide.
Binary in configuration files and environment variables
Configuration formats such as YAML, TOML and .env files are text. Certificates, private keys in DER form, and small binary secrets are therefore stored as Base64. A PEM file is exactly this idea: a block of Base64 between header and footer lines. Container orchestration systems commonly store secret values as Base64 too, which leads to one of the most persistent myths covered below: that this makes them secret.
Data URIs
A data URI embeds a file directly into a document, using the form data:[mediatype][;base64],data. For example:
data:text/plain;base64,SGVsbG8=
A browser decodes that to the text Hello. For images, the same pattern lets you place a small icon directly in HTML or CSS without a separate request. The trade-offs are worth understanding, and we cover them in the next section.
Other places
You will also meet Base64 in cryptographic key exchange formats, in signed cookies, in WebAuthn and other credential APIs that exchange binary values, in source maps that inline their contents, in HTTP headers that carry binary metadata, and in database dumps and logs where binary columns have to be printed. The common thread is always the same: a binary value and a channel that only trusts text.
Data URIs: when inlining pays off and when it does not
Inlining a small image removes an HTTP request. For a handful of tiny assets, such as a 200-byte icon, that can be a net win. For larger assets, the costs dominate.
The costs are concrete:
- 33 percent more bytes. The encoded image is larger than the original file, and an image format that is already compressed, such as PNG or JPEG, does not shrink when Base64 text is gzipped much beyond undoing the encoding overhead. In our measurement below, gzip brought encoded random bytes back to about the size of the original, not below it.
- No separate caching. An image in a stylesheet or HTML page is cached only as part of that file. Change one pixel and every visitor downloads the whole bundle again. A separate image file can be cached for a long time on its own.
- Blocking and parsing. A large inline blob in a stylesheet delays the parsing of everything after it. CSS is render-blocking, so a heavy data URI in it can slow the first paint.
- Harder to debug and diff. A line holding a 40 KB string makes version control diffs and editor performance worse.
- Modern protocols reduce the benefit. With HTTP/2 and HTTP/3, many small requests share one connection, so the cost that data URIs avoided is smaller than it used to be.
A useful rule of thumb is to inline only assets that are tiny, used on nearly every page, and critical to first render, and to serve everything else as normal files. For SVG, you often do not need Base64 at all, because SVG is already text. A percent-encoded SVG in a data URI is usually smaller than its Base64 form. Our SVG formatting and editing guide covers how to keep SVG source clean, and the CSS minification and critical CSS guide explains how to decide what belongs in the first render.
What Base64 is not
Four misconceptions cause real incidents. Clear them up and you will avoid most Base64 mistakes.
Base64 is not encryption
Encryption needs a key. Base64 has no key. The mapping is public and fixed, so anyone can reverse it with a single function call. A secret stored as Base64 is a secret written in a public cipher, which is to say, not a secret. The best-known example is Kubernetes-style secret objects, where values are Base64 to carry arbitrary bytes inside YAML. People see unreadable text and assume protection. Access control and encryption at rest must come from elsewhere.
Base64 is not compression
Compression makes data smaller by removing redundancy. Base64 makes it larger, by one third, every time. We measured it directly:
const crypto = require("crypto"), zlib = require("zlib");
const random = crypto.randomBytes(30000);
const text = Buffer.from("The quick brown fox jumps over the lazy dog. ".repeat(700));
for (const [name, b] of [["random", random], ["text", text]]) {
const e = b.toString("base64");
console.log(name, b.length, e.length, zlib.gzipSync(b).length, zlib.gzipSync(e).length);
}
The output columns are raw bytes, Base64 characters, gzip of the raw bytes, and gzip of the Base64 text:
| Sample | Raw | Base64 | Gzip of raw | Gzip of Base64 |
|---|---|---|---|---|
| 30,000 random bytes | 30,000 | 40,000 | 30,028 | 30,160 |
| 31,500 bytes of repeated text | 31,500 | 42,000 | 170 | 223 |
Two things stand out. Base64 grew both samples by exactly 33.33 percent. And gzip recovers the loss only partially: random data stays at its original size at best, and even highly repetitive text ended up larger when encoded first (223 bytes against 170). The lesson is to compress first, and encode last, if you have a choice. Compressing after encoding still helps a little with repetitive data, but as the table shows, it cannot match compressing the original.
Base64 is not security
There is no integrity check in Base64. Flipping a character produces a different decoded value, or an error, and nothing in the format tells you which. If you need to know that data has not been altered, use a signature or a message authentication code. If you need confidentiality, use encryption. Base64 can then carry the result of either, but it adds neither.
Base64 is not a text format for text
Using Base64 on ordinary text to "make it safe" is usually wrong. If the text is already valid for the channel, encoding it just obscures it and inflates it. Use proper escaping for the channel instead: percent-encoding for URLs, entity escaping for HTML, quoting for JSON. Base64 is for bytes that cannot otherwise be represented.
Encoding and decoding in code
All examples in this section were executed with Node.js 22 and Python 3.13, and the comments show the real output.
JavaScript in the browser: btoa and atob
Browsers provide btoa to encode and atob to decode. The names are old and confusing: "binary to ASCII" and "ASCII to binary".
console.log(btoa("Hello, World!")); // SGVsbG8sIFdvcmxkIQ==
console.log(atob("SGVsbG8sIFdvcmxkIQ==")); // Hello, World!
These functions treat each character of a string as one byte, and they only accept characters in the range 0 to 255, which is the Latin-1 range. Anything beyond that throws:
try { btoa("héllo €"); } catch (e) { console.log(e.name, e.message); }
// InvalidCharacterError Invalid character
The euro sign has code point 8364, which does not fit in a byte. This is the most common Base64 bug in browser code, and the fix is to encode the text to bytes yourself first.
Unicode text with TextEncoder and TextDecoder
Convert the string to UTF-8 bytes with TextEncoder, turn each byte into a character, and give the result to btoa. To decode, reverse the process:
const enc = (s) => {
const bytes = new TextEncoder().encode(s);
let bin = "";
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin);
};
const dec = (s) => {
const bin = atob(s);
return new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0)));
};
console.log(enc("héllo €")); // aMOpbGxvIOKCrA==
console.log(dec(enc("héllo €"))); // héllo €
Note what btoa("héllo") does without this step: it succeeds, because every character fits in a byte, and returns aOlsbG8=. That is the Latin-1 encoding of the word, not UTF-8. Another system that decodes it as UTF-8 will see a broken character. The error for the euro sign is annoying, but the silent success for é is the more dangerous behaviour, because the bug survives until it meets a different decoder.
Building a string one character at a time is fine for small inputs. For large byte arrays, loop in chunks, because passing millions of arguments to String.fromCharCode.apply can overflow the call stack.
JavaScript in Node.js: Buffer
Node's Buffer handles bytes directly, so there is no Latin-1 trap. It also supports base64url:
console.log(Buffer.from("Hello, World!").toString("base64"));
// SGVsbG8sIFdvcmxkIQ==
console.log(Buffer.from("SGVsbG8", "base64").toString());
// Hello
The second line is worth noticing. The input SGVsbG8 has no padding, and Node decodes it anyway. Node is lenient: it skips whitespace and accepts missing padding. Leniency is convenient, but it can hide malformed input, so validate when you need strictness.
Python: the base64 module
Python's base64 module works on bytes, so you must encode text to UTF-8 first:
import base64
print(base64.b64encode(b"Hello, World!")) # b'SGVsbG8sIFdvcmxkIQ=='
print(base64.b64decode("SGVsbG8sIFdvcmxkIQ==")) # b'Hello, World!'
d = bytes([0xfb, 0xff, 0xfe])
print(base64.b64encode(d), base64.urlsafe_b64encode(d))
# b'+//+' b'-__-'
s = "héllo €".encode("utf-8")
e = base64.b64encode(s)
print(e, base64.b64decode(e).decode("utf-8"))
# b'aMOpbGxvIOKCrA==' héllo €
The result of the encode function is bytes, not str. Call .decode("ascii") when you need a string for JSON or a header. The UTF-8 output matches the JavaScript output exactly, aMOpbGxvIOKCrA==, which is how you confirm two implementations agree.
A hand-rolled encoder, verified against the library
Writing the algorithm yourself is the best way to understand it. This encoder implements exactly the steps from the worked example:
const A = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function encode(bytes) {
let out = "";
for (let i = 0; i < bytes.length; i += 3) {
const n = bytes.length - i; // bytes left in this group
const b0 = bytes[i];
const b1 = n > 1 ? bytes[i + 1] : 0;
const b2 = n > 2 ? bytes[i + 2] : 0;
const v = (b0 << 16) | (b1 << 8) | b2; // 24 bits
out += A[(v >> 18) & 63] + A[(v >> 12) & 63]
+ (n > 1 ? A[(v >> 6) & 63] : "=")
+ (n > 2 ? A[v & 63] : "=");
}
return out;
}
console.log(encode(Buffer.from("Man")), encode(Buffer.from("Ma")), encode(Buffer.from("M")));
// TWFu TWE= TQ==
let ok = true;
for (let len = 0; len < 200; len++) {
const b = require("crypto").randomBytes(len);
if (encode(b) !== b.toString("base64")) ok = false;
}
console.log("matches Buffer for lengths 0 to 199:", ok);
// matches Buffer for lengths 0 to 199: true
The shift-and-mask pattern is the whole algorithm. Shifting right by 18, 12, 6 and 0 and masking with 63 pulls out each six-bit group from the 24-bit value. Missing input bytes are treated as zeros, and the characters that would depend only on those zeros become =. The test compares the result against the built-in encoder for 200 random inputs of every length from 0 to 199, which covers all three padding cases many times over.
Streaming large inputs in Python
Because Base64 works on independent three-byte groups, you can encode a stream in chunks as long as every chunk except the last has a length divisible by three. Otherwise, padding appears in the middle of the output.
import base64, io, os
def stream(inp, out, chunk=3 * 1024):
while True:
b = inp.read(chunk)
if not b:
break
out.write(base64.b64encode(b))
data = os.urandom(100000)
o = io.BytesIO()
stream(io.BytesIO(data), o)
print(o.getvalue() == base64.b64encode(data), len(o.getvalue()))
# True 133336
The chunk of 3,072 bytes is a multiple of three, so the chunked output is identical to encoding the whole input at once, which the True confirms. The length, 133,336, matches the formula: 4 times the ceiling of 100,000 divided by 3. With this pattern, memory use stays constant regardless of file size.
Command line
Most systems ship a base64 utility, and both Node and Python can be run from the shell for a quick check. Behaviour varies between platforms, especially line wrapping and the flag used to decode, so check your local manual page before putting it in a script. Where you need identical behaviour across machines, a few lines of Python or Node are more portable than shell tools.
Step-by-step: encode and decode with DevUtilX
For a quick job with no code and no setup, use the browser tools. Both run entirely in your browser, so the data you paste is not uploaded to a server.
Encoding text or data
- Open the DevUtilX Base64 Encoder.
- Paste or type your input into the input area.
- Run the encoder.
- Check the output. It should contain only letters, digits,
+,/and trailing=characters. - Copy the result to wherever you need it.
If your text includes accented letters, emoji or non-Latin scripts, the encoder should handle it as UTF-8. You can confirm by comparing against the examples above: the text héllo € should give aMOpbGxvIOKCrA==.
Decoding
- Open the DevUtilX Base64 Decoder.
- Paste the Base64 string. Surrounding whitespace and line breaks from an email or PEM block are usually fine to include.
- Run the decoder.
- Read the output. If it is readable text, you are done. If it looks like random symbols, the original data was probably binary, such as an image or a compressed file, and the decoder cannot show it as text.
Because processing happens locally, you can inspect a token or a config value without sending it anywhere. Still, treat real credentials with care. Do not paste a live production secret into any tool unless you need to, and rotate it if it has been exposed. If the decoder reports an error, jump to the troubleshooting section below. And if the decoded text turns out to be JSON, the formatter described in our JSON formatting guide makes it readable.
Common mistakes and pitfalls
Most Base64 bugs fall into a short list.
- Calling it encryption. Covered above. It protects nothing.
- Using
btoaon Unicode text. It throws for characters above 255, and quietly produces Latin-1 for characters between 128 and 255. Always convert to UTF-8 bytes first. - Mixing the standard and URL-safe alphabets. A token with
-and_fed into a standard decoder fails or yields wrong bytes. A standard string with+and/placed unescaped in a URL gets mangled. Know which alphabet each side expects. - Forgetting or mishandling padding. Some producers strip it, some require it, and some decoders reject a missing one. Normalise before decoding.
- Leaving whitespace and newlines in. MIME-style wrapping, copy-paste from a terminal, and trailing newlines from files can all introduce characters a strict decoder rejects. Strip whitespace first, or use a decoder that ignores it.
- Treating decoded bytes as text. Decoded Base64 is bytes. If the original was an image, forcing it into a string corrupts it. Keep it as a byte array or buffer until you know its type.
- Decoding as the wrong text encoding. The bytes might be UTF-8, UTF-16 or Latin-1. If the decoded text shows
éwhere you expectedé, you decoded UTF-8 bytes as Latin-1. - Encoding twice. Applying Base64 to something already Base64 gives a longer, valid-looking string that decodes to another Base64 string. Count the layers when a decoded value still looks encoded.
- Putting large files in JSON. The cost is a third more bytes and more memory on both ends. Prefer a URL.
- Concatenating encoded chunks carelessly. Joining two padded strings does not produce a valid encoding of the joined data in a strict decoder, because of the padding in the middle.
- Logging encoded secrets. Anyone with log access can decode them. Treat encoded secrets as plain text for logging purposes.
Performance and memory
Base64 is cheap per byte, but it is not free, and the costs are easy to forget because they are spread across a system.
Size on the wire. Add one third to anything you encode. A 6 MB image becomes 8 MB of text. If the receiver also decodes it into a buffer, the sender and receiver each briefly hold both forms.
Memory. In a browser, encoding a large file with FileReader and readAsDataURL produces a string roughly one third larger than the file, and that string lives on the JavaScript heap. Converting it again to other forms creates more copies. Prefer Blob and object URLs when you only need to display a file, since they reference the data without turning it into a string. Server code that reads a whole file, encodes it, and builds a JSON document holds the raw bytes, the encoded string and the final serialised text at once.
CPU. Encoding and decoding are linear in the data size, and browsers and runtimes implement them natively, so for typical payloads the time is negligible. The cost matters when you do it for very large inputs on the main thread, where it blocks the UI. Move large conversions into a Web Worker or process them in chunks.
Compression interaction. As the measurements above show, Base64 of already-compressed or random data is effectively incompressible beyond recovering the encoding overhead. Compress before encoding when you control both steps, and apply HTTP-level compression to the whole response. The relationship is the same one discussed in the JSON minification guide.
Streaming. Process data in chunks whose length is a multiple of three when encoding, as in the Python example. For decoding, use chunks whose length is a multiple of four, and make sure whitespace does not split a group. Streaming keeps memory constant and lets the first bytes flow before the last are read.
Alternatives for big binary data. If you routinely move megabytes of binary, Base64 is the wrong tool. Send the bytes as binary with the right content type, use multipart form data for uploads, or use a binary protocol. Base64 is a bridge for small and medium values that must stay inside text.
Security considerations
- Secrets in Base64 are not hidden. Treat any Base64 value as if it were plain text. A credential, API key or private key that is only encoded is exposed to everyone who can read it. That includes repository history, logs, container manifests and browser storage.
- Decoding untrusted input. Treat decoded bytes as untrusted input, exactly as you would the raw data. A Base64 string that decodes to a script, an HTML fragment, a serialised object or a file path is still an attack vector once decoded. Validate size, type and content after decoding, never before only.
- Limit input size. An attacker can send a very long string and force a large allocation on decoding. Cap the length of encoded input at your boundary, and remember that the decoded size is about three quarters of the encoded size.
- Beware decoders that skip invalid characters. Lenient decoders, including Python's default
b64decode, discard characters outside the alphabet. We ranbase64.b64decode("SGVs\nbG8=\n")and gotb'Hello'. That leniency can let malformed or smuggled content pass a check that was applied to the raw string. In Python, passvalidate=Truefor strict behaviour: with it,SGV$bG8=raisedbinascii.Error: Only base64 data is allowed. - Do not use it for obfuscation. Hiding a license check or a feature flag behind Base64 adds no protection, and it makes the code harder to review.
- Use a signature or MAC for integrity. If data travels through an untrusted channel, authenticate it. JWT does this with its signature segment.
- Data URIs and content security. User-supplied data URIs in links or images can carry active content in some contexts. If your application accepts them, restrict allowed media types and consider a content security policy that limits where
data:sources are permitted. - Never expose Basic credentials over plain HTTP. The Base64 encoding of
username:passwordis trivially reversible, so the transport must be encrypted.
Accessibility and usability
Base64 has few accessibility concerns of its own, but data URIs do. An image inlined as a data URI still needs meaningful alternative text in the alt attribute, since the file name that might otherwise hint at its purpose is gone. Strings that users must copy by hand, such as recovery codes or API keys, are error-prone in Base64 because 0 and O, or l and I, look alike. For values that humans read aloud or retype, consider encodings designed for it, such as Base32, which avoids those ambiguous characters and ignores case.
Troubleshooting guide
Work from the symptom to the cause.
Symptom: "Invalid character" or InvalidCharacterError
Cause. In a browser, btoa was given a character above code point 255, such as € or an emoji. Alternatively, atob received a character outside the alphabet, such as a - or _ from a URL-safe string, or a stray $.
Fix. For encoding, convert to UTF-8 bytes with TextEncoder first. For decoding, replace - with + and _ with / before calling atob, and strip anything that is not part of the alphabet.
Symptom: "Incorrect padding" in Python
Cause. The string length is not a multiple of four, usually because padding was stripped, as in base64url tokens. We reproduced it: base64.b64decode("SGVsbG8") raised binascii.Error: Incorrect padding.
Fix. Restore the padding before decoding:
def fix(s):
return s + "=" * (-len(s) % 4)
print(base64.b64decode(fix("SGVsbG8"))) # b'Hello'
The expression -len(s) % 4 yields the number of missing equals signs: zero, one, two or three. A remainder that requires three signs means the input is truncated or corrupt, since a length of 4n plus one cannot occur in valid data.
Symptom: the decoded text shows é instead of é
Cause. UTF-8 bytes were interpreted as Latin-1. This is the mirror image of the btoa mistake, and it happens when you call atob and use the result directly as text.
Fix. After atob, convert the binary string to a byte array and decode with TextDecoder, as in the dec function above. In Python, call .decode("utf-8") on the bytes.
Symptom: a token from a library will not decode in another
Cause. One side uses the URL-safe alphabet, the other the standard one, or padding conventions differ. A JWT segment contains - or _ and the decoder expects + and /.
Fix. Use the matching variant explicitly: urlsafe_b64decode in Python, Buffer.from(s, "base64url") in Node, or translate the characters in the browser. A tolerant helper is:
function fromBase64Url(s) {
s = s.replace(/-/g, "+").replace(/_/g, "/");
while (s.length % 4) s += "=";
return atob(s);
}
Symptom: decoding fails after copying from an email or PEM file
Cause. The text contains line breaks, spaces or header lines such as -----BEGIN CERTIFICATE-----.
Fix. Remove the header and footer lines, and strip whitespace. Many decoders accept line breaks, but the dashed lines are not Base64.
Symptom: decoded data is garbage, not text
Cause. The original was binary, such as an image, compressed data, or an encrypted value. There is nothing wrong with the decoding. Text is simply the wrong way to view the bytes.
Fix. Write the bytes to a file or inspect the first few bytes. Many formats start with recognisable signatures. If the data is compressed or encrypted, apply the next step, such as decompression, with the right tool.
Symptom: output has a newline in the middle
Cause. The encoder wrapped lines at 76 characters, as MIME does, or a command line tool did by default.
Fix. Use a non-wrapping encoder function, or remove the newlines before embedding the string in JSON, a header or a data URI.
Symptom: the string decodes, but to something that is still Base64
Cause. The value was encoded twice, often because one layer encoded it and another layer encoded the result again.
Fix. Decode again, then find and remove the redundant encoding step. A double-encoded value is about 78 percent larger than the original, since each layer adds a third.
Symptom: padding appears in the middle of the output
Cause. A streaming encoder processed chunks whose lengths were not multiples of three and joined the results.
Fix. Use chunk sizes that are multiples of three, or use an encoder object that keeps state between chunks.
Best practices and checklist
- Use Base64 only when you must carry binary through a text channel. If the channel supports binary, send binary.
- Never rely on it for secrecy or integrity. Pair it with encryption or signatures when you need those.
- Encode text as UTF-8 before encoding as Base64, and decode as UTF-8 after.
- Choose the alphabet deliberately. Use base64url for anything that goes into a URL, a filename or a token, and the standard alphabet elsewhere.
- Decide on padding as a policy. Follow the specification you implement, and make decoders tolerant of both forms.
- Keep large files out of JSON and CSS. Serve them as separate resources so they can be cached and compressed properly.
- Inline only tiny, critical assets as data URIs, and measure the result.
- Compress before encoding when you control both steps.
- Stream big inputs in chunks that are multiples of three bytes.
- Validate after decoding. Check size, type and content of the decoded bytes.
- Cap input length at your boundaries.
- Test round trips. Encode then decode a set of inputs of every length modulo three, including the empty string, and compare.
- Use the library, not your own encoder, except for learning. A hand-written version is a fine exercise and a poor choice for production code.
Before you ship code that handles Base64, run through these questions:
- Is the data really binary, or would escaping do?
- Which alphabet does the other side expect, and does it expect padding?
- Are non-ASCII characters converted to UTF-8 bytes on both ends?
- Is the encoded size acceptable, with the extra third accounted for?
- Is anything secret stored only as Base64?
- Is decoded input validated and size-limited?
- Is there a round-trip test for lengths 0, 1, 2 and 3?
Related encodings and alternatives
Base64 is not the only way to put bytes into text. The trade-offs differ.
Hexadecimal (Base16)
Hex writes each byte as two characters from 0-9 and a-f. The word Hello becomes 48656c6c6f, which is ten characters for five bytes, an overhead of 100 percent. Its advantages are simplicity and readability: you can read byte values directly. It is the right choice for hashes, colour codes, memory dumps and anything a human inspects byte by byte.
Base32
Base32 uses 32 symbols, five bits per character, and groups five bytes into eight characters. Hello becomes JBSWY3DP, for an overhead of 60 percent. The alphabet in RFC 4648 uses uppercase letters and the digits 2 to 7, which avoids look-alikes such as 0 and O, and it survives case-insensitive systems. It is used where humans type or speak the values, such as one-time password secrets in authenticator apps.
Base85 and Ascii85
Base85 packs four bytes into five characters, an overhead of 25 percent, which is better than Base64. The cost is a larger alphabet that includes punctuation which may need escaping in some contexts, and several incompatible variants. Python provides two of them: a85encode turned Hello into 87cURDZ, and b85encode gave NM&qnZv. It appears in PDF and PostScript, and in some version control tooling.
Percent-encoding
URLs use % followed by two hex digits for unsafe bytes. It suits text with a few special characters, where Base64 would be heavier. For arbitrary binary, it can triple the size in the worst case.
Raw binary with the right transport
Multipart form data, binary WebSocket frames, application/octet-stream responses and binary protocols carry bytes with no encoding overhead at all. When the transport allows it, this is the most efficient option.
Comparison table
| Encoding | Output per 3 input bytes | Overhead | Alphabet | Best for |
|---|---|---|---|---|
| Raw binary | 3 bytes | 0% | Any byte | Binary-capable transports |
| Base85 | about 3.75 characters | 25% | 85 printable characters | Compact text-safe binary |
| Base64 | 4 characters | 33% | A-Z a-z 0-9 + / |
General binary in text, email, JSON, data URIs |
| base64url | 4 characters (padding optional) | 33% or less | A-Z a-z 0-9 - _ |
URLs, filenames, tokens |
| Base32 | 4.8 characters | 60% | A-Z 2-7 |
Human-typed, case-insensitive values |
| Hex | 6 characters | 100% | 0-9 a-f |
Hashes, debugging, readable bytes |
| Percent-encoding | 3 to 9 characters | 0% to 200% | Unreserved characters and %XX |
Mostly-text URL components |
The overhead figures for Base32, hex and Base64 follow directly from the group sizes, and the Base85 figure from four bytes mapping to five characters. For Hello itself, the measured lengths were 10 characters in hex, 8 in Base32 without its padding (JBSWY3DP), and 8 in Base64 (SGVsbG8=).
FAQ
Is Base64 encryption?
No. It has no key and the mapping is public. Anyone can decode a Base64 string with one function call. Use real encryption when you need confidentiality.
Why does Base64 end with equals signs?
They are padding. The encoder works in groups of three bytes, and when the input does not fill the last group, one or two = characters fill the output to a multiple of four. TWE= represents two bytes and TQ== represents one.
Can I remove the padding?
Often yes, if both sides agree. JWT and many URL-oriented formats omit it, and decoders can recompute how many characters are missing from the length. A strict decoder that requires padding will reject the stripped form, so restore it before decoding when needed.
Why is Base64 about 33 percent bigger than the original?
Three bytes, which are 24 bits, become four characters of one byte each. That is a ratio of four to three. With padding and MIME line breaks the real overhead is slightly higher, and for tiny inputs it is much higher in relative terms.
What is the difference between Base64 and base64url?
Base64url replaces + with - and / with _ so the output is safe in URLs and filenames without escaping, and it commonly omits the = padding. The underlying algorithm is the same, and both are defined in RFC 4648.
Why does btoa throw an error on some text?btoa accepts only characters up to code point 255. Characters such as €, or emoji, are outside that range. Convert the string to UTF-8 bytes with TextEncoder, build a binary string from those bytes, and encode that.
Can I encode an image or a PDF with Base64?
Yes. Any binary file can be encoded. Read the file as bytes, not as text, and encode the bytes. The result is about a third larger than the file, and you can embed it in JSON or as a data URI.
Should I use data URIs for all my images?
No. They suit tiny, critical assets. Larger images inflate your HTML or CSS, cannot be cached separately and delay rendering. Serve them as normal files.
How do I decode a JWT?
Split on the dots, then decode the first two segments with base64url, restoring padding if your decoder needs it, and parse the result as JSON. Decoding does not verify the signature. Verification needs the key and a proper library.
Is it safe to decode Base64 from an untrusted source?
The decoding itself is safe, but the result is untrusted data. Limit the input size, validate the decoded content and do not execute or deserialise it blindly.
Why does my decoded text look like random symbols?
The original was probably binary, such as an image, compressed data or ciphertext. Base64 decoding recovered the exact bytes, but they are not text. Save them to a file or inspect them as bytes.
Does Base64 keep my data private if it is in a URL?
No. It only makes the characters URL-safe. Anyone who sees the URL can decode it, and URLs are stored in logs, browser history and referrer headers.
Is Base64 the same in every language?
The standard alphabet and algorithm are the same, so the same bytes give the same output. Differences appear in defaults: line wrapping, padding for URL-safe output, and how strictly decoders treat whitespace and invalid characters.
What is the difference between Base64 and hex?
Hex uses two characters per byte, a 100 percent overhead, while Base64 uses four characters per three bytes, about 33 percent. Hex is easier for people to read byte by byte. Base64 is more compact.
Conclusion
Base64 is a small, precise tool. It turns bytes into a safe subset of text at the price of one third more size, and that is all it does. Understanding the mechanics, three bytes into four six-bit groups with = to fill the last block, removes most of the mystery, and knowing what it is not will keep secrets out of places they should not be. Pick base64url for URLs and tokens, convert text to UTF-8 before encoding, and keep large files out of JSON and stylesheets.
When you need a quick conversion, try the DevUtilX Base64 Encoder and Base64 Decoder. They run in your browser, so your input stays on your machine. To keep going, read our guide on JSON minification and payload size to see how Base64 blobs affect response sizes, and the HTML minification guide to weigh what belongs inline in a page.
Further reading
Try the tools
Encode your strings into the Base64 format for secure data transmission. This tool is essential for handling binary data in text-based formats, such as in emails or APIs, ensuring data integrity during transport.
Decode Base64 strings back to their original form. This tool is crucial for developers who need to interpret and process data that has been encoded in the Base64 format for various applications.


