Base64 exists to move bytes through channels that only accept text: email bodies, JSON strings, HTTP headers, URLs, HTML attributes. It maps every 3 bytes of input to 4 characters from a 64-character alphabet that survives almost any text handling. That is all it does.
To follow the examples as you read, encode and decode Base64 in your browser. The encoder works on text (as UTF-8) and has a URL-safe option; the decoder accepts the standard and URL-safe alphabets, missing padding and line breaks.
Base64 is encoding, not encryption. Anyone can decode it, with no key and no effort. Treat a Base64 value as plain text that is merely harder to read at a glance.
How 3 bytes become 4 characters
Base64 works on 24-bit groups. Take 3 bytes (24 bits), split them into four 6-bit numbers between 0 and 63, and look each one up in the alphabet: A–Z are 0–25, a–z are 26–51, 0–9 are 52–61, + is 62 and / is 63.
The word "Man" is the classic example:
Text M a n
Bytes 0x4d 0x61 0x6e
Bits 01001101 01100001 01101110
6-bit 010011 010110 000101 101110
Value 19 22 5 46
Character T W F u
Result TWFu
Decoding runs the same steps backwards: each character gives back 6 bits, and every 4 characters give back 3 bytes.
Padding and the = sign
When the input length isn't a multiple of 3, the last group is short. The encoder fills the missing bits with zeros and appends = so the output length stays a multiple of 4:
"Man" 3 bytes → TWFu
"Ma" 2 bytes → TWE= 16 bits fill 3 characters, plus 1 pad
"M" 1 byte → TQ== 8 bits fill 2 characters, plus 2 pads
Padding carries no data. A decoder can work out how many bytes the last group holds from the length alone, which is why many formats drop it. What no decoder can handle is a length that leaves one character over (5, 9, 13 characters and so on), because a single character holds only 6 bits, less than a byte. If the devprix decoder says a value "leaves one dangling character", the string was almost certainly cut off when it was copied. It also catches = in the middle of the input, which usually means two Base64 values were pasted together.
Size: why Base64 is a third bigger
Four output characters for every three input bytes is a 33% increase, rounded up to a whole group of four. The encoded length is 4 × ceil(n / 3):
10 bytes → 16 characters
30,720 bytes → 40,960 characters (a 30 KB image becomes 40 KB of text)
Line wrapping adds more. MIME email limits lines to 76 characters and ends each one with CRLF, which brings the overhead to about 37%. This is why inlining images as data URIs only makes sense for small ones: the bytes grow, and the image can no longer be cached separately from the page or stylesheet that contains it.
Text is bytes first: UTF-8
Base64 encodes bytes, not characters. Before you can encode text you have to choose a character encoding, and on the web that should be UTF-8. The choice changes the output:
"héllo" as UTF-8 68 c3 a9 6c 6c 6f → aMOpbGxv
"héllo" as Latin-1 68 e9 6c 6c 6f → aOlsbG8=
"👋" as UTF-8 f0 9f 91 8b → 8J+Riw==
This bites in JavaScript, because btoa() doesn't use UTF-8. It treats each character as a single byte, so it silently produces the Latin-1 result for "héllo" and throws InvalidCharacterError for anything above U+00FF, such as an emoji. Its counterpart atob() returns one character per byte, so UTF-8 text comes back garbled: atob("aMOpbGxv") returns "héllo". Go through bytes explicitly:
// Browser
const bytes = new TextEncoder().encode("héllo");
btoa(String.fromCharCode(...bytes)); // "aMOpbGxv"
// Node.js
Buffer.from("héllo", "utf8").toString("base64"); // "aMOpbGxv"
# Python
base64.b64encode("héllo".encode("utf-8")) # b'aMOpbGxv'
For large inputs, convert the bytes in chunks; spreading a big array into String.fromCharCode can exceed the engine's argument limit. The devprix encoder handles all of this for you. When you decode something that isn't valid UTF-8, it labels the result as binary data, with unreadable bytes shown as �, so you know you're looking at a file rather than broken text.
Binary files and data URIs
Images, PDFs and other binary files are already bytes, so there is no character encoding to choose. You can often recognise them from the first few characters: PNG data starts with iVBORw0KGgo and JPEG data with /9j/, because those are the Base64 forms of their file signatures.
For images, use the Image to Base64 converter rather than a text encoder. It reads the file directly and gives you a data URI (data:image/png;base64,...), raw Base64, an HTML <img> tag, a CSS background-image rule or Markdown. It shows the original and encoded sizes with the percentage growth, and can resize the image before encoding. It also works in reverse: paste Base64 or a data URI to preview the image and download it.
On the command line, GNU base64 on Linux wraps output at 76 characters by default, so use base64 -w 0 file.png when you need a single line for JSON or an environment variable. On macOS, base64 -i file.png prints a single line unless you ask for breaks with -b.
URL-safe Base64
Two characters in the standard alphabet cause trouble in URLs and file names: + and /. RFC 4648 section 5 defines a "URL and filename safe" alphabet, usually called base64url, that replaces them with - and _. Everything else is identical:
Text <<???>>
Standard PDw/Pz8+Pg==
URL-safe PDw_Pz8-Pg
The padding usually goes too. An = is normally percent-encoded in a URL (as %3D), and the length already tells the decoder where the data ends, so formats such as JWT leave it out: all three parts of a signed JWT are base64url without padding. That's why the header {"alg":"HS256","typ":"JWT"} shows up as eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 at the start of so many tokens. The guide on how to read JWT claims picks up from there.
Here is what happens when standard Base64 goes into a query string unescaped:
?token=PDw/Pz8+Pg==
new URLSearchParams("token=PDw/Pz8+Pg==").get("token")
// "PDw/Pz8 Pg==" the + became a space
URLSearchParams and many server frameworks parse query strings as form data, where + means a space, so the value is corrupted before your code sees it. Either use base64url, or percent-encode the standard value (PDw%2FPz8%2BPg%3D%3D) with the URL encoder. In the devprix Base64 encoder, tick URL-safe to get -, _ and no padding; the decoder recognises either alphabet automatically.
Line breaks in MIME and PEM
Base64 in email (MIME, RFC 2045) is split into lines of at most 76 characters. PEM files, the -----BEGIN CERTIFICATE----- blocks, wrap at 64. Paste one of these into a strict decoder and it rejects the first newline. The devprix decoder ignores line breaks and other whitespace, so you can paste wrapped Base64 as it is. Leave out the BEGIN and END lines, though: they are labels, not Base64, and the decode fails if you include them.
Base64 is not encryption
This gets its own section because the mistake keeps shipping. Base64 hides nothing:
btoa("username:password") // "dXNlcm5hbWU6cGFzc3dvcmQ="
atob("dXNlcm5hbWU6cGFzc3dvcmQ=") // "username:password"
An HTTP Basic Authorization header carries exactly that: the word Basic and the Base64 of username:password, which is why it is only acceptable over HTTPS. Kubernetes Secret manifests store values as Base64, and anyone who can read the manifest can read the secret. A JWT payload is readable by anyone holding the token. If you need confidentiality, encrypt the data with a real cipher such as AES-GCM, then Base64 the ciphertext if it has to travel as text.
Next steps
Paste a suspicious value from your logs or config into the Base64 decoder to see what it really holds. If it turns out to be a JWT, read JWT decoder vs JWT verifier to understand why reading a token is not the same as trusting it.