The core mechanic: one character in, %XX out
Percent encoding does exactly one thing, and understanding it removes most of the confusion. Take a character, look at the numeric value of each byte that represents it, write that value in two hexadecimal digits, and stick a % in front. That is the whole algorithm.
Two consequences fall straight out of this. First, the % character itself has to be encoded (as %25) whenever you want a literal percent sign, otherwise a decoder would treat it as the start of an escape. Second, a character that takes more than one byte — every non-ASCII character in UTF-8 — becomes multiple %XX pairs, one per byte. The letter é (UTF-8 bytes C3 A9) becomes %C3%A9, not a single token.
Which characters get encoded: the RFC 3986 table
The single most useful thing to memorize is the character classification from RFC 3986. There are only three buckets, and the space between them explains almost every "why is this encoded and that isn't" question.
| Character class | Members / examples | Encode it? | RFC 3986 category |
|---|---|---|---|
| Unreserved | A–Z, a–z, 0–9, - _ . ~ | Never — always safe as-is | unreserved |
| Reserved (delimiters) | : / ? # [ ] @ ! $ & ' ( ) * + , ; = | Only when used as literal data, not as a delimiter | reserved (gen-delims + sub-delims) |
| Other ASCII (unsafe) | space, " < > % { } | \ ^ ` | Always | must-encode |
| Non-ASCII | é, 你, 😀, any byte above 0x7F | Always — as UTF-8 bytes, one %XX per byte | must-encode |
The interesting bucket is the middle one. Reserved characters are the URL's punctuation — ? separates the path from the query, & separates one query parameter from the next, / separates path segments. When you mean them as punctuation, leave them alone. When they appear inside your data, they must be encoded so they aren't mistaken for structure. A search for cats & dogs has to become ?q=cats%20%26%20dogs, because an unencoded & would start a second parameter and split your value in half.
Where URL encoding actually happens
You rarely encode by hand, and you shouldn't. Encoding runs silently every time you submit a web form, every time a search engine turns your query into a URL, and every time an API client attaches parameters to a request. The job of a developer is mostly to encode user input before it goes into a URL — because that input can contain anything.
Use your language's built-in functions rather than replacing characters yourself:
- JavaScript:
encodeURIComponent(value)for a single query value or path segment;encodeURI(url)for an already-assembled URL you must not break. - Python:
urllib.parse.quote(value)for path segments;urllib.parse.quote_plus(value)for form-style query values (encodes spaces as+). - Command line:
[System.Uri]::EscapeDataString('hello world')in PowerShell, or a one-linepython3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read()))'on Linux/macOS.
These functions handle the edge cases — multi-byte UTF-8, the reserved-vs-unreserved distinction, the percent sign itself — that hand-written string replacement almost always gets wrong.
Space is a special case: %20 vs. +
One recurring confusion deserves its own note. A space can appear as either %20 or +, and both are correct in the right place. In the general URL syntax of RFC 3986, a space is encoded as %20. But in the older application/x-www-form-urlencoded format used for HTML form submissions, a space is encoded as +, and a literal plus sign is then written %2B. Modern code paths mostly standardize on %20, but if you see + in a query string it is almost certainly a form encoding, and a decoder for form data will turn it back into a space. When in doubt, %20 is the safe, universally understood form.
Encoding is not security
It is worth stating plainly because the mistake is common: URL encoding is for correctness, not protection. It guarantees a URL parses the way you intended; it does nothing to sanitize the meaning of the data inside.
Two failure modes matter. Double-encoding attacks exploit systems that decode more than once — a filter blocking ../ for directory traversal can miss %252e%252e%252f, which decodes first to %2e%2e%2f and then to ../. And context confusion: a value that is perfectly safe in a URL is still dangerous when later dropped into HTML (needs HTML encoding to stop XSS) or into SQL (needs parameterized queries). Always validate and sanitize input after decoding, and apply the encoding appropriate to each destination context rather than treating URL encoding as a security control.
Key takeaways
- Percent encoding turns a character into
%plus its byte value in two hex digits; a space is%20because 32 decimal is 20 hex. - RFC 3986 sorts characters into unreserved (never encode), reserved (encode only when used as data), and everything else (always encode).
- Non-ASCII characters are encoded as their UTF-8 bytes — one
%XXpair per byte, so é is%C3%A9. - Use
encodeURIComponent,urllib.parse.quote, or an equivalent built-in; never hand-roll encoding. - Encoding is about syntactic correctness, not security — still validate, sanitize, and re-encode for each context.
Ready to encode or decode a URL right now? Use our URL Encoder / Decoder tool for instant, accurate percent encoding with automatic UTF-8 handling — all processed client-side in your browser so your URLs never leave your device.