A little help goes a long way. Find a tool/

Developer FIELD NOTES · 4 MIN READ

URL Encoding: When %20 Matters

How percent-encoding works: which characters may ride bare, why spaces become %20 or +, how %XX hides UTF-8 bytes, the double-encoding trap, and how to parse a URL into parts.

A URL is an address with grammar: certain characters are punctuation that separates parts, and everything that is not punctuation must travel in a safe disguise. URL encoding — percent-encoding — is that disguise, and the difference between knowing and guessing it is the difference between a working query string and one that silently truncates at the first &.

What may ride bare, and what must be dressed

The grammar divides characters into two camps. Unreserved characters can appear anywhere literally: letters, digits, and - _ . ~ (encoders also leave ! ' ( ) * alone). Everything else is either reserved — punctuation with a job, like ? & = # / : — or simply unsafe to transmit, like a space. Reserved characters are the tricky half: a & in a query value is legal text but fatal grammar, because it terminates the value and starts a new parameter. So when "rock & roll" travels as a search term, it must become rock%20%26%20roll — the space and the ampersand both dressed. The same string sent bare would parse as two parameters, rock and roll, which is exactly the class of bug nobody notices in testing.

%20 versus +

Two encodings exist for a space, and they belong to different layers. %20 is the universal percent-encoding, correct everywhere in a URL. + means "space" only inside query strings submitted by HTML forms (the application/x-www-form-urlencoded convention), which is why a search box can produce ?q=rock+%26+roll and browsers will decode it back to "rock & roll". The trap is scope: a + in a path is just a plus, and some servers treat a + in a query as a literal plus. When in doubt, %20 is never wrong — it decodes identically everywhere and cannot be misread as addition.

Percent-encoding hides bytes, not characters

The %XX sequences are hexadecimal bytes, which matters the moment text leaves plain ASCII. The é in "café" is not one byte: in UTF-8 it is two bytes, C3 A9, so the encoded form is caf%C3%A9 — and "café menu" becomes caf%C3%A9%20menu. Multi-byte scripts grow accordingly, which is why a Japanese query can balloon to three times its visible length. Decoding reverses this exactly: decodeURIComponent reads the bytes and reconstructs the characters, so caf%C3%A9%20menu comes back as "café menu". The byte-level story of why é is two bytes lives in How Text Encoding Works.

The double-encoding trap

Encode a string that is already encoded and you get %2520: the % itself becomes %25, and the "20" rides along as text. Decode that once and you see %20 — looking broken but actually half-decoded. The bug usually enters through a component that encodes defensively (a framework, a redirect, a template helper) and meets a developer who encoded manually "just in case". The hygiene rules: encode exactly once, at the boundary where the value enters the URL; decode exactly once, where it leaves; and if you must handle text of unknown history, decode repeatedly until nothing percent-shaped remains, then encode once. A batch decoder makes auditing a list of copied links — campaign URLs, exported logs — a one-glance job.

Parsing: the parts a URL is made of

Encoding exists to protect the boundary lines of a URL's grammar, so it helps to see the grammar. Take https://ultraquick.tools/tools/url-encoder?ref=learn#how: the scheme is https:, the host is ultraquick.tools, the path is /tools/url-encoder, the query is ?ref=learn, and the fragment is #how. A parser returns each part separately — and crucially decodes the query parameters one by one, so ?q=rock%26roll&page=2 yields q = "rock&roll" and page = "2", not one mangled string. Parsers also handle the everyday conveniences: a missing scheme is usually assumed to be https, and the fragment never reaches the server, since the browser consumes it locally. This is also why reserved characters are reserved — each one marks a boundary between these parts, as How URL Slugs Work shows from the path side.

Try it yourself

Type "rock & roll" or "café menu" into the URL Encoder & Decoder and watch which characters get dressed and which ride bare, then decode the results back. Paste a handful of encoded links — campaign URLs, log excerpts, %2520 casualties — into the URL Decode Batch to decode them all at once, and dissect any full URL into scheme, host, path, query, and fragment with the URL Parser to see the grammar the encoding protects.

Frequently asked questions

When should a space be %20 and when +?
%20 is always correct — it is the universal percent-encoding and decodes identically everywhere. + means a space only inside query strings under the form-encoding convention, and a + in a path is just a literal plus. When in doubt, use %20; it cannot be misread.
Why does é become %C3%A9?
Percent-encoding hides bytes, not characters. In UTF-8 the é is two bytes, C3 A9, so it encodes as %C3%A9 and its word grows visibly. Non-Latin scripts stretch further — each character can become three or more byte triplets, which is why encoded non-English queries get long fast.
What is double encoding and why is %2520 broken?
Encoding already-encoded text turns the percent sign itself into %25, so a space becomes %2520. One decode leaves %20 behind — half-decoded and looking broken. Encode exactly once where the value enters the URL, decode exactly once where it leaves, and for unknown-history text decode repeatedly until nothing percent-shaped remains.
Which characters can appear in a URL without encoding?
Unreserved characters: letters, digits, hyphen, underscore, dot, and tilde (encoders also leave exclamation mark, asterisk, apostrophe, and parentheses alone). Everything else is reserved punctuation like ? & = # / : or unsafe, and must be percent-encoded whenever it appears as data rather than grammar.
Does the URL fragment (#how) get sent to the server?
No. The browser keeps the fragment to itself — it scrolls to the anchor or drives client-side routing locally. Servers, logs, and analytics never see it, which is why fragments cannot carry data you need server-side and never need encoding on the wire.