Type a space into a URL and something downstream turns it into %20. URL encoding — percent-encoding, to use the specification's name — is that mechanism, and it exists for one reason: a URL is not just a string. It is a structured thing held together by delimiters, and any data you drop into it has to be stopped from looking like one of them.

Once you see it that way, the cases that feel arbitrary stop being arbitrary. The rules come from RFC 3986, and there are fewer than you would expect.

Why URL encoding turns a space into %20

Percent-encoding has exactly one form, given in RFC 3986 section 2.1: a percent sign followed by two hexadecimal digits giving the byte value — "%" HEXDIG HEXDIG. The space character is byte 0x20 in ASCII. Hex 20. So it becomes %20. Nothing special about spaces; the number falls out of the ASCII table, the same way & at 0x26 becomes %26.

A space has to be encoded at all because it is not in the set of characters a URL may contain as-is. RFC 3986 defines an unreserved set — ALPHA / DIGIT / "-" / "." / "_" / "~" — and anything outside it is either a reserved delimiter or must be percent-encoded.

Beyond ASCII the rule is two steps, set out in section 2.5: encode the character as UTF-8 octets first, then percent-encode each octet that is not unreserved. The RFC's own examples are %C3%80 for LATIN CAPITAL LETTER A WITH GRAVE and %E3%82%A2 for KATAKANA LETTER A — one character, two or three escapes, which is why encoded non-Latin text looks so much longer than the original.

Reserved, unreserved, and the rule that follows

RFC 3986 sorts punctuation into two reserved groups plus the unreserved set:

And then the rule that governs everything else: if data for a URI component would conflict with a reserved character's purpose as a delimiter, then the conflicting data must be percent-encoded before the URI is formed.

Read that wording carefully. It does not say reserved characters are banned, but to encode one when your data would otherwise be mistaken for structure. Which gives the practical rule everything else rests on: you encode a value going into a URL component, not the finished URL. Running an encoder across a complete URL destroys the delimiters that made it a URL.

A character is only reserved where it sits

This is the part that looks inconsistent until you know why. Whether a character needs encoding depends on which component it lands in.

Common characters and their encodings

CharacterEncodedWhy it matters
space%20Never legal raw; form data uses +
#%23Starts the fragment, never sent to the server
%%25The escape character; source of double-encoding
&%26Separates query parameters
+%2BRead as a space in form-encoded data
/%2FSeparates path segments
=%3DSeparates a query key from its value
?%3FStarts the query string
:%3ADelimits scheme and port
@%40Delimits userinfo in the authority

Letters, digits, and - . _ ~ are the unreserved set and are never encoded. To check a specific string rather than read it off a table, the URL encoder and decoder on this site runs in your browser and shows both directions.

encodeURI vs encodeURIComponent

JavaScript gives you two functions, and you almost always want the second one.

That difference is the entire point. encodeURI is for a URL you have already assembled and believe well-formed; encodeURIComponent is for one value you are about to drop into it. MDN's guidance is direct: to assemble string values into a URL dynamically, use encodeURIComponent() on each dynamic segment.

MDN's example makes the failure concrete. With a value of Ben & Jerry's, calling encodeURI on the whole assembled URL leaves the ampersand intact, so ?choice=Ben%20&%20Jerry's parses as two parameters and choice comes back as Ben with a trailing space. Encoding just the value gives ?choice=Ben%20%26%20Jerry's, which round-trips correctly. Note the apostrophe survives untouched — it is in that exception list.

The plus sign, and why form data is different

Here is the one that catches everyone. HTML form submissions and query strings built with URLSearchParams use application/x-www-form-urlencoded, which is not quite RFC 3986: in that format a space is written as + rather than %20. The WHATWG URL Standard specifies it in the serializer — if the space-as-plus flag is set and the byte is 0x20, append +.

So a literal plus sign in your data must be encoded as %2B, or it comes back as a space. In the browser:

MDN states it plainly: the URLSearchParams constructor interprets plus signs as spaces, which might cause problems. This is also why base64 breaks in query strings with no error to warn you — standard base64 uses + as one of its 64 characters, so a payload pasted raw into a query comes back with spaces where plus signs were. Use searchParams.append(), which encodes the plus as %2B, or switch to base64url. Worth remembering that base64 is not encryption either, just a transport-safe re-spelling of bytes.

Double encoding, and how %20 becomes %2520

Encode a string twice and the escape character gets escaped. % is byte 0x25, so % becomes %25, and therefore %20 becomes %2520 — the %25 standing in for the percent sign, followed by the literal characters 2 and 0.

RFC 3986 section 2.4 is explicit: implementations must not percent-encode or decode the same string more than once, because decoding an already-decoded string can misread a percent data octet as the start of an escape, and the confusion runs in reverse when encoding. In practice it happens when a value is encoded by your application code and then again by a framework helper, a redirect handler, or a CDN rewrite rule. The symptom is a visible %20 in rendered text, or a download named report%20final.pdf.

Fix it by finding which layer is doing the second encode, not by adding a decode at the end — a blanket extra decode corrupts every value that legitimately contained a percent sign. And %2520 is not automatically a bug: if the data really did contain the three characters %20, then %2520 is correct. You cannot tell from the string alone; you have to know what the value is meant to be. Which layer owns the escaping is the same question that makes JSON escaping worth being deliberate about.

Try the URL Encoder / Decoder

Percent-encode or decode any string — runs entirely in your browser, nothing uploaded.

Open URL Encoder →

Frequently asked questions

Why do spaces become %20 in a URL?

Because a space is not in the set of characters a URL may contain unencoded, and percent-encoding represents a byte as a percent sign plus two hex digits. The space character is byte 0x20 in ASCII, so it is written as %20. RFC 3986 defines the unreserved set that may appear as-is: letters, digits, hyphen, period, underscore and tilde.

Should I use encodeURI or encodeURIComponent?

Almost always encodeURIComponent. It escapes the URL syntax characters such as ampersand, equals, question mark and slash, which is exactly what you need when inserting a value into a query string or path segment. encodeURI deliberately leaves those characters alone because it assumes you are handing it a complete, already well-formed URL rather than a single value.

Why does my plus sign turn into a space?

Because form data uses application/x-www-form-urlencoded, where a space is written as a plus sign rather than %20. The WHATWG URL Standard specifies this, and MDN notes that the URLSearchParams constructor interprets plus signs as spaces. To keep a literal plus, encode it as %2B — for example by using searchParams.append() rather than building the query string by hand.

What causes %2520 in a URL?

Double encoding. The percent sign is itself byte 0x25, so encoding an already-encoded string turns %20 into %2520. RFC 3986 says implementations must not encode or decode the same string more than once. Track down which layer is encoding twice rather than adding a decode at the end, since a blanket decode will corrupt values that genuinely contain a percent sign.

Related reading