URL Encoding Explained: The Rules and the Bug Everyone Hits Eventually
Those %20s and %3Fs in a URL aren't random — they're percent-encoding, the mechanism that lets URLs safely carry characters that would otherwise break their structure or fall outside the ASCII character set URLs are restricted to per RFC 3986.
How percent-encoding actually works
- Identify a character that's unsafe or structurally reserved
- Convert it to its byte value (typically UTF-8)
- Represent each byte as two hex digits
- Prefix with
%
A space (ASCII decimal 32, hex 20) becomes %20. This is mechanical and consistent — the same character always encodes to the same value.
Reserved vs. unsafe characters — a real distinction
Reserved characters have structural meaning in a URL: ? starts a query string, & separates parameters, = separates a key from its value, / separates path segments, # marks a fragment. These only need encoding when they appear as data rather than as structure — encoding the & that separates two query parameters would break the query string, but a literal & inside one parameter's value must be encoded, or it gets misread as a new parameter starting.
This distinction is the source of a real, common bug: encoding an entire URL (including its structural characters) versus encoding just a piece of data that will be inserted into a URL are different operations, and using the wrong one either breaks the URL or fails to protect the data.
The bug: encodeURI() vs encodeURIComponent()
In JavaScript, these are not interchangeable, and mixing them up is a genuinely common source of broken links:
const url = "https://example.com/search?q=coffee & tea";
encodeURI(url);
// "https://example.com/search?q=coffee%20&%20tea"
// Leaves structural characters (: / ? &) alone - use this on a FULL url
encodeURIComponent("coffee & tea");
// "coffee%20%26%20tea"
// Encodes EVERYTHING including & = ? - use this on a single VALUE
// going into a query parameter
The practical rule: use encodeURIComponent() on individual values you're inserting into a URL (a search term, a parameter value), and never on a complete URL — it'll also encode the :// and ? you need intact. Use encodeURI() only when you have a full URL and want to leave its structure alone while still encoding unsafe characters within it. Getting this backwards is the most common cause of a query parameter either breaking the URL or arriving at the server with the wrong value.
Python has the equivalent distinction:
from urllib.parse import quote, quote_plus, urlencode
quote_plus("coffee & tea") # "coffee+%26+tea" - space as +, for form data
quote("coffee & tea") # "coffee%20%26%20tea" - space as %20, for URL paths
urlencode({"q": "coffee & tea"}) # "q=coffee+%26+tea" - full query string, handles the key too
Reference table
| Character | Description | Encoded |
|---|---|---|
| (space) | Space | %20 |
! | Exclamation | %21 |
" | Double quote | %22 |
# | Hash | %23 |
$ | Dollar | %24 |
% | Percent | %25 |
& | Ampersand | %26 |
' | Single quote | %27 |
( ) | Parentheses | %28 %29 |
* | Asterisk | %2A |
+ | Plus | %2B |
, | Comma | %2C |
/ | Forward slash | %2F |
: | Colon | %3A |
; | Semicolon | %3B |
= | Equals | %3D |
? | Question mark | %3F |
@ | At sign | %40 |
[ ] | Brackets | %5B %5D |
Note: in application/x-www-form-urlencoded data (standard form submissions), a space is conventionally encoded as + rather than %20 — this is why quote_plus and quote differ above, and why you'll see both conventions depending on context.
URL structure and SEO, briefly
- Keep slugs simple and readable (
/blog/url-encoding-guideover/blog/post?id=12345) - Use hyphens for word separation in slugs — search engines parse hyphens as word boundaries more reliably than other separators, and hyphens never need encoding in the first place
- Keep core paths free of characters that require encoding — reserve encoding for actual dynamic parameter values, not the path structure itself
- Stick to lowercase in paths — some servers are case-sensitive, and inconsistent casing creates accidental duplicate URLs
Quick encode/decode without writing code
For a one-off check, ToolSink's URL Encoder/Decoder handles both directions directly in the browser — useful for quickly checking what a messy tracking URL or query string actually contains.
Conclusion
Percent-encoding itself is mechanical and simple; the actual skill is knowing whether you're encoding a full URL (preserve structure, use encodeURI) or a single value going into one (encode everything, use encodeURIComponent). That distinction, not the reference table, is where most real bugs come from.