Base64 and URL encoding explained: what they are for, and when each is the wrong tool
Base64 and percent-encoding are the two transformations every developer meets in the first month and a surprising number never quite pin down. They look similar — text goes in, uglier text comes out — and they solve entirely different problems. Base64 exists because some channels only carry text and your data is bytes. Percent-encoding exists because some characters have a structural job inside a URL and your data needs to contain them anyway. Confuse the two, or apply either in the wrong place, and you get the classic bugs: a name that arrives as Jos%C3%A9, a query parameter that truncates at the first ampersand, an emoji that crashes the encoder, a password “secured” by Base64. This guide covers what each one actually does, with the exact behaviour of the Base64 encoder, Base64 decoder, URL encoder and URL decoder on this site, and ends with the cases where both are the wrong tool entirely.
Base64: bytes through a text-only pipe
Base64 takes arbitrary bytes and rewrites them using 64 characters that survive anything: A–Z, a–z, 0–9, + and /. It processes three bytes (24 bits) at a time and emits four characters of six bits each. When the input is not a multiple of three, the output is padded with = so the length stays a multiple of four:
"abc" → YWJj (3 bytes → 4 chars, no padding)
"ab" → YWI= (2 bytes → 4 chars, one pad)
"a" → YQ== (1 byte → 4 chars, two pads)That four-for-three ratio is the famous overhead: Base64 output is about 33% larger than the input, before any line breaks. It buys compatibility with channels that were built for text and would otherwise mangle bytes — email bodies (Base64 is a MIME transfer encoding), JSON string fields, XML elements, HTTP headers, source code, and anywhere a null byte or a stray 0x0D would end the value early.
The historical reason those channels exist is worth a sentence, because it explains the design: SMTP was specified for 7-bit ASCII, so the eighth bit of every byte was not safe to transmit. Base64 encodes into a 64-character subset that no gateway would rewrite. The problem is solved today and the encoding remains, because “text-safe bytes” is a permanently useful thing to have.
Two alphabets, and the one that catches people out
The last two characters of the standard alphabet, + and /, are both meaningful inside a URL, and = is meaningful inside a query string. So RFC 4648 defines a second alphabet, Base64url: - instead of +, _ instead of /, and padding usually dropped. This is what JWT segments, many API keys and most URL-embedded tokens use.
The two are not interchangeable, and the failure is abrupt: paste a Base64url string into a standard decoder and it answers Invalid Base64 input, because - and _ are not in its alphabet. Converting by hand is three substitutions — - to +, _ to /, then pad with = until the length divides by four. If you are working with a token, the JWT decoder does that automatically.
One more variant: MIME Base64 (RFC 2045) wraps output at 76 characters with CRLF line breaks, which is why a certificate or an email attachment arrives as a block of short lines. The decoder here strips all whitespace before decoding, so pasting a wrapped block works without cleaning it up first.
UTF-8 first — the emoji bug
Base64 encodes bytes, and text is not bytes until you choose an encoding. This is where the browser primitive trips people. btoa() accepts only characters in the range 0–255, so:
btoa('👋') → DOMException: Invalid character
btoa('café') → DOMException: Invalid characterThe correct sequence is text → UTF-8 bytes → Base64, and the widespread workarounds that mask the error instead of fixing it are how é becomes é two systems downstream. The encoder on this site runs TextEncoder first, then Base64-encodes the resulting bytes in 32 KB chunks (large inputs would otherwise blow the JavaScript call stack), so emoji, Greek, Arabic and CJK all round-trip exactly. The decoder reverses it with TextDecoder. If a decode produces  at the start or é in the middle, the bytes were not UTF-8 in the first place — the mistake happened before Base64 was involved.
Base64 is not security. Not even slightly
There is no key. Decoding is a mechanical step anyone can run, including the tool on this page. Two places this matters in practice:
- HTTP Basic authentication. The
Authorization: Basicheader isuser:passwordin Base64. That is transport convenience, not protection — over plain HTTP the credentials are effectively in the clear, which is why Basic auth is only acceptable over TLS. - “Obfuscated” config values. A Base64-encoded API key in a repository is a plain-text API key in a repository. Secret scanners decode it; so does anyone who reads the file.
The legitimate uses are all about transport and embedding: attachments in email, binary fields inside JSON, certificates in PEM form (which is exactly Base64-wrapped DER between two header lines), and data: URLs. On that last one — a data:image/png;base64,… URL saves an HTTP request at the cost of 33% more bytes, no independent caching, and a document that cannot finish parsing until the whole blob is read. Worth it for a 500-byte icon; a bad trade for anything you would otherwise put behind a CDN.
Percent-encoding: making characters safe inside a URL
A URL is a structured string: ? starts the query, & separates parameters, = splits name from value, / separates path segments, # starts the fragment. When a value needs to contain one of those, it has to be escaped, or the URL parser will read it as structure. Percent-encoding replaces the character with % plus its byte in hexadecimal — and for non-ASCII characters, that means one %XX per UTF-8 byte:
space → %20
& → %26
é → %C3%A9 (two bytes)
👋 → %F0%9F%91%8B (four bytes)The characters that never need escaping — the unreserved set — are A–Z a–z 0–9 - . _ ~. JavaScript’s encoders also leave ! * ' ( ) alone for historical reasons, which is occasionally a problem with strict server-side parsers that expect those escaped.
Component or whole URI: the choice that matters
The URL encoder has a mode selector with exactly this choice, and picking wrong is the single most common percent-encoding bug. The difference is which structural characters are left intact:
| Input | Component (query values) | Full URI |
|---|---|---|
a b | a%20b | a%20b |
& | %26 | & (left alone) |
/ | %2F | / (left alone) |
? # = + | %3F %23 %3D %2B | left alone |
[ ] " < > | escaped | escaped |
So: Component for a single value you are about to drop into a query string or path segment; Full URI for a complete URL you want to tidy up without destroying its structure. Encoding a search term with Full URI mode is the bug that lets a query like tea & coffee silently become two parameters, with everything after the ampersand landing somewhere you did not intend.
The decoder mirrors both modes. A malformed sequence — a stray %, or a truncated %E0%A4%A — is a hard error rather than a best-effort guess, and the page says Invalid percent-encoding (malformed URI sequence). That usually means the string was cut short, or that something percent-encoded it twice and then decoded it once.
Double encoding, and %20 versus +
Double encoding is the classic symptom that a value passed through two encoders and one decoder. Watch the percent sign itself: % encodes to %25, so a space encoded twice becomes %2520, and a URL that reads https%3A%2F%2Fexample.com in the address bar has been encoded once too often. If you see %25 anywhere in a value that should not contain a literal percent, run the decoder once and compare.
The other recurring confusion is the plus sign. In application/x-www-form-urlencoded — the classic HTML form POST body, and some query strings that follow the same convention — a space is encoded as +. That rule belongs to the form-encoding format, not to URIs generally: percent-encoders emit %20, and URI decoders leave + as a literal plus. This is why a name typed into an old form can arrive as Anne+Marie, and why a plus inside an email address must be sent as %2B or it may come back as a space.
When both are the wrong tool
Escaping is contextual: the right transformation depends entirely on where the value is going. Percent-encoding a string does nothing to make it safe inside HTML, and Base64 makes nothing safe anywhere.
| Destination | Correct escaping |
|---|---|
| URL path or query value | Percent-encoding, component mode |
| HTML text or attribute | HTML entity escaping (<, &, ") — or a templating engine that does it for you |
| JavaScript string in a page | JSON serialisation, then HTML escaping of the result |
| SQL query | Parameterised queries. Not escaping. Ever |
| Shell command | Pass arguments as an array; if you must build a string, single-quote and escape embedded quotes |
| CSV cell | Double the quotes and wrap the field |
All four tools here run in the page — the text you paste is processed by your own browser and never sent anywhere, which matters more than it sounds when the thing you are decoding turns out to be a token, a signed URL or a customer’s email address.
Do this
- Encode text to UTF-8 bytes before Base64, always — that is what makes emoji and accents survive.
- Use component mode for a single query value; full-URI mode only for a whole URL you are tidying. Never full-URI mode on a value containing
&. - For URL-embedded Base64, use the Base64url alphabet (
-and_); translate it back before feeding a standard decoder. - Treat
%2520or a visible%25as proof of double encoding, and decode exactly once for each encode. - Remember
+means space only in form-encoded bodies; send a literal plus as%2B. - Never use Base64 as protection — for secrets, use encryption and a key you manage.
Frequently asked questions
Is Base64 a form of encryption?
No. Base64 is a public, reversible encoding with no key — anyone can decode it in one step. It exists to carry binary data through channels that only accept text, not to hide anything. If a system stores passwords or tokens in Base64 and calls that protection, it is storing them in plain text with extra steps.
Why did my emoji or accented characters break when I Base64-encoded them?
The browser primitive btoa() only accepts characters in the 0–255 range, so it throws on anything outside Latin-1, and code that works around it by truncating produces mojibake. The correct order is text → UTF-8 bytes → Base64. The encoder here does exactly that with TextEncoder, which is why emoji survive a round trip.
What is the difference between encodeURI and encodeURIComponent?
encodeURIComponent escapes everything that is not unreserved, including / ? : @ & = + $ , and # — use it for one value you are dropping into a query string. encodeURI leaves those structural characters alone because it assumes you handed it a whole URL. Use the first for parts, the second for wholes, and never encodeURI a value that might contain an ampersand.
Should a space become %20 or +?
In a URL path or a modern query string, %20. The plus sign means space only in application/x-www-form-urlencoded content — HTML form submissions and some older APIs. Percent-encoders do not produce +, and decoders that follow the URI spec do not turn + back into a space, which is why a name pasted from a form sometimes arrives with plus signs in it.
When is a data: URL worth it?
For assets of roughly a couple of kilobytes or less that are needed immediately — a tiny inline icon, a placeholder image — where saving a request beats the costs. Base64 adds about 33% to the size, the data cannot be cached separately from the document that contains it, and a large inline blob delays parsing of everything after it. Anything bigger belongs in its own cacheable file.
Tools used in this guide
Every one of these runs in your browser — the files you work on never leave your device.