Base64 vs URL Encoding: When to Use Which

· Updated September 10, 2026 · by webmaster

Base64 and URL encoding both turn awkward data into safe text, so they are easy to mix up, and the mix-ups cause confusing bugs. They solve different problems. Base64 turns any bytes into a fixed alphabet of 64 characters. URL encoding (percent-encoding) escapes only the characters a URL cannot contain and leaves everything else alone. This guide shows what each does at the byte level, where the variants differ, how the JavaScript, PHP and Python functions behave, and when you need both at once. You can try each example in the Base64 encoder and URL encoder as you read.

Base64 maps every 3 bytes to 4 characters

Base64 takes bytes in groups of three (24 bits), splits them into four groups of six bits, and maps each 6-bit value to one of 64 characters: A-Z, a-z, 0-9, + and /. When the input length is not a multiple of three, the encoder pads the output with =.

For example, the string Hi! is the bytes 48 69 21 in hex:

Bytes:   01001000 01101001 00100001
Regroup: 010010 000110 100100 100001
Values:  18     6      36     33
Chars:   S      G      k      h

So Hi! becomes SGkh. Base64 can represent every byte value from 0 to 255. That is why it is the standard way to move binary data (images, keys, compressed blobs) through systems that only accept text: JSON, XML, email, HTTP headers and environment variables.

Base64 is not encryption, so never use it to hide anything. Anyone can reverse it with a Base64 decoder in about a second. It also makes the data about a third larger, and that overhead adds up fast once you inline images.

Base64 comes in three common variants

VariantCharacters 62 and 63PaddingLine breaksWhere you meet it
Standard (RFC 4648 §4)+ /=NoneData URIs, HTTP Basic auth, JSON payloads
URL-safe (RFC 4648 §5)- _Usually omittedNoneJWT, OAuth PKCE, URL parameters
MIME (RFC 2045)+ /=CRLF every 76 charactersEmail attachments, PEM certificates

The bytes FB FF BF encode to +/+/ in standard Base64 and to -_-_ in the URL-safe alphabet. The bits are the same in both. The URL-safe alphabet exists because +, / and = all have a meaning inside a URL. The JWT article shows how tokens use the URL-safe variant so they can travel in headers and query strings without further encoding.

URL encoding escapes only the characters a URL cannot hold

A URL is limited to a subset of ASCII. RFC 3986 splits the allowed characters into two groups. The unreserved set is A-Z a-z 0-9 - . _ ~, and those never need encoding. The reserved set is : / ? # [ ] @ ! $ & ' ( ) * + , ; =. Those characters have structural meaning, so you must encode them when they appear as data rather than as delimiters.

Everything else, including spaces and any non-ASCII character, must be percent-encoded: each UTF-8 byte of the character is written as % followed by two hex digits.

space  -> %20
&      -> %26
é      -> C3 A9 in UTF-8 -> %C3%A9

Encoding a b&c=d/é as a single value produces a%20b%26c%3Dd%2F%C3%A9. The space, ampersand, equals sign and slash are all escaped, because inside a value they would read as delimiters. Every safe character stays as it is, and this is the big practical difference from Base64. In JavaScript, Hi! stays Hi!. The exclamation mark is a sub-delimiter, and the languages disagree about it: encodeURIComponent leaves it alone, while PHP’s rawurlencode and Python’s quote both turn it into %21.

Form encoding writes spaces as a plus sign

application/x-www-form-urlencoded is the format browsers use for HTML form bodies, and it is what URLSearchParams produces. It encodes spaces as + instead of %20, and a literal + becomes %2B. Everywhere else in a URL, %20 is correct. The URL decoder follows decodeURIComponent and leaves + alone, so swap + for a space first if the string came from a form post. For example, a name that arrives on the server as “Mary+Ann” is this mismatch at work.

encodeURI vs encodeURIComponent: use the component version for values

JavaScript has two encoding functions, and picking the wrong one is a common source of URL bugs.

encodeURIComponent("a b&c=d/é");
// a%20b%26c%3Dd%2F%C3%A9

encodeURI("https://example.com/a b?q=a&b=é");
// https://example.com/a%20b?q=a&b=%C3%A9

encodeURIComponent("https://example.com/a b?q=1");
// https%3A%2F%2Fexample.com%2Fa%20b%3Fq%3D1

Use encodeURIComponent for any single value. It encodes everything except A-Z a-z 0-9 - _ . ! ~ * ' ( ), so it is the right call for a query parameter, a path segment or a header value.

encodeURI also leaves ; , / ? : @ & = + $ # alone. Use it only on a complete URL you assembled from trusted parts, where you want to fix spaces and non-ASCII without breaking the structure. Running it on a user-supplied value is a bug. An & in the value will split your query string, and it will do so only for the one user whose input contained an ampersand, which makes it hard to reproduce.

For query strings, skip both functions and use URLSearchParams. It handles the & and = separators for you:

const params = new URLSearchParams({ q: "a b&c", lang: "é" });
params.toString(); // q=a+b%26c&lang=%C3%A9

Note the + for the space: URLSearchParams uses form encoding.

Base64 and URL encoding in JavaScript, PHP and Python

JavaScript: btoa needs UTF-8 bytes first

// Base64 (browser and Node)
btoa("Hi!");                                     // SGkh
atob("SGkh");                                    // Hi!

// btoa only handles Latin-1; encode UTF-8 bytes first for other text
const bytes = new TextEncoder().encode("café");
btoa(String.fromCharCode(...bytes));             // Y2Fmw6k=

// Node has Buffer with both alphabets
Buffer.from("café").toString("base64");          // Y2Fmw6k=
Buffer.from("café").toString("base64url");       // Y2Fmw6k

// Percent-encoding
encodeURIComponent("café");                      // caf%C3%A9
decodeURIComponent("caf%C3%A9");                 // café

The btoa limitation causes frequent errors. It throws on anything outside Latin-1, so the TextEncoder step is required for real-world text.

PHP: prefer rawurlencode

<?php
base64_encode("café");                  // Y2Fmw6k=
base64_decode("Y2Fmw6k=");              // café

// URL-safe variant by hand
$std = base64_encode("\xfb\xff\xbf");   // +/+/
rtrim(strtr($std, '+/', '-_'), '=');    // -_-_

rawurlencode("a b&c=d/é");   // a%20b%26c%3Dd%2F%C3%A9  (RFC 3986)
urlencode("a b&c=d/é");      // a+b%26c%3Dd%2F%C3%A9    (form encoding)
rawurldecode("caf%C3%A9");   // café

// Query strings: form encoding by default, RFC 3986 on request
http_build_query(['q' => 'a b&c', 'lang' => 'é']);
// q=a+b%26c&lang=%C3%A9
http_build_query(['q' => 'a b&c', 'lang' => 'é'], '', '&', PHP_QUERY_RFC3986);
// q=a%20b%26c&lang=%C3%A9

Prefer rawurlencode for anything that goes in a path or a modern API. urlencode exists for legacy form bodies.

PHP has one extra trap. json_encode escapes forward slashes by default, so a standard Base64 blob comes out of an API as "+\/+\/" rather than "+/+/". It decodes correctly either way, but that escaping is why it looks wrong in a response body. JSON_UNESCAPED_SLASHES turns it off.

Python: quote keeps the slash by default

import base64
from urllib.parse import quote, quote_plus, unquote, urlencode

base64.b64encode("café".encode()).decode()                           # Y2Fmw6k=
base64.b64decode("Y2Fmw6k=").decode()                                # café
base64.urlsafe_b64encode(b"\xfb\xff\xbf").decode()                   # -_-_
base64.urlsafe_b64encode(b"\xfb\xff\xbf\xfb").decode()               # -_-_-w==
base64.urlsafe_b64encode(b"\xfb\xff\xbf\xfb").rstrip(b"=").decode()  # -_-_-w

quote("a b&c=d/é")            # a%20b%26c%3Dd/%C3%A9  (slash is kept by default)
quote("a b&c=d/é", safe="")   # a%20b%26c%3Dd%2F%C3%A9
quote_plus("a b&c=d/é")       # a+b%26c%3Dd%2F%C3%A9
unquote("caf%C3%A9")          # café
urlencode({"q": "a b&c", "lang": "é"})  # q=a+b%26c&lang=%C3%A9

Python’s quote keeps / unless you pass safe="", so a value that happens to contain a slash comes out partly unencoded. urlsafe_b64encode also keeps the = padding, so strip it yourself for JWT-style output.

Combine them when Base64 has to travel in a URL

Sometimes you need both encodings, and the order matters.

JWT is the clean case. A token is Base64URL(header).Base64URL(payload).signature, where the signature is usually HMAC-SHA256. The URL-safe alphabet contains no reserved characters, so the token can sit in an Authorization header or a query string without further encoding. Had the spec used standard Base64, every + and / would need percent-encoding, and a missed = would corrupt the token.

If you have standard Base64 and must put it in a URL, percent-encode it after the Base64 step. Y2Fmw6k= then becomes Y2Fmw6k%3D. Skipping this step is the usual reason a trailing = has gone missing by the time the value reaches the server.

Data URIs can carry either Base64 or percent-encoded content:

data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4IiBoZWlnaHQ9IjgiPjxyZWN0IHdpZHRoPSI4IiBoZWlnaHQ9IjgiIGZpbGw9InJlZCIvPjwvc3ZnPg==

data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%228%22%20height%3D%228%22%3E%3Crect%20width%3D%228%22%20height%3D%228%22%20fill%3D%22red%22%2F%3E%3C%2Fsvg%3E

For binary formats such as PNG or WebP, Base64 is the only option. The image to Base64 tool gives you the Base64 payload, which you prefix with data:image/png;base64, to get a usable URI. SVG is text, so either encoding works. In this small example the raw SVG is 106 bytes, the Base64 version is 144 characters, and the percent-encoded version is 180. Percent-encoding only wins when most characters are already URL-safe. If you inline a lot of SVG icons, you can cut the cost further by leaving <, > and " unencoded inside a single-quoted CSS url('...').

HTTP Basic auth is the opposite case: Authorization: Basic followed by standard Base64 of username:password, with no percent-encoding at all, because headers are not URLs.

Base64 adds 33% while percent-encoding can triple the size

InputRaw bytesBase64Percent-encoded
café589
300 random bytes300400up to 900
106-byte SVG106144180

Base64 grows data by a fixed 4/3, about 33 percent, plus padding. Percent-encoding costs nothing for unreserved ASCII and triples every other byte, which means up to 200 percent overhead for binary data. Use Base64 for binary data and percent-encoding for text that is already mostly URL-safe.

Decision guide: which encoding to use

You haveYou needUse
Binary dataTo put it in JSON, XML, a header or an env varStandard Base64
Binary dataTo put it in a URL or a JWTBase64URL (no padding)
Binary dataTo attach it to an email or a PEM fileMIME Base64 with 76-character lines
A text valueTo put it in a query string or path segmentencodeURIComponent, rawurlencode, quote(safe="")
A full URLTo fix spaces and non-ASCII in a URL you assembledencodeURI
A form bodyTo submit key-value pairsURLSearchParams, urlencode, quote_plus
Standard Base64To put it in a URLPercent-encode it after Base64 encoding

When you are unsure, paste the value into the URL encoder or the Base64 encoder and check the exact string that will go over the wire.

Tools used in this guide

Copied to clipboard!