"Which characters actually need encoding?" is a narrower question than how percent-encoding works, and it deserves a direct, scannable answer — not everyone hitting this page wants the mechanism, some just want to check whether @ is safe to leave alone. Here's the reference.
The short list that's always safe
Per RFC 3986, exactly these characters are unreserved — never encoded, in any part of a URL, by any tool that follows the spec:
A–Z a–z 0–9 - _ . ~Everything else is either reserved (has structural meaning somewhere in a URL, so whether it needs encoding depends on where it sits) or falls outside ASCII entirely (always needs encoding, no exceptions).
The reserved characters, and what each one means
RFC 3986 splits reserved characters into two groups. gen-delims mark the major structural boundaries of a URL:
| Character | Marks |
|---|---|
: | Scheme separator (https:), and port (host:port) |
/ | Path segment separator |
? | Start of the query string |
# | Start of the fragment |
[ ] | Wrap an IPv6 host literal |
@ | Separates userinfo from the host |
sub-delims are reserved for use within a single URL component — most commonly seen doing double duty in query strings:
| Character | Typical meaning |
|---|---|
& | Separates parameters in a query string |
= | Separates a parameter's key from its value |
+ | Means a literal space, in a query string specifically (see below) |
; | Historically an alternate parameter separator in some systems |
! $ ' ( ) * , | No universal structural role — reserved for scheme-specific use |
A reserved character is only dangerous where it could be confused with the role it plays above. An & inside a query parameter value will be misread as the next parameter starting — it needs encoding there. The same & sitting in a path segment isn't reserved for anything a path parser looks for, so it can be left alone. There is no single yes/no answer per character — it depends on which part of the URL it's in.
What JavaScript's built-in encoders actually leave alone
In practice, most encoding happens through encodeURIComponent or encodeURI, and each has an exact, fixed safe set — worth knowing precisely rather than guessing:
| Function | Leaves unescaped | Use for |
|---|---|---|
encodeURIComponent | A–Z a–z 0–9 - _ . ! ~ * ' ( ) | A single value — a query parameter, a path segment |
encodeURI | A–Z a–z 0–9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # | A complete URL, where / ? : @ & = # etc. are doing real structural work |
Note the mismatch with strict RFC 3986: both functions additionally leave ! ~ * ' ( ) unescaped even though the spec calls them reserved — a historical quirk from an earlier RFC 2396 definition of "unreserved" that the JavaScript spec never updated. They're harmless to leave unescaped in practice, but don't assume every URL-handling library treats them the same way — some encoders do escape them.
Quick answer by where the value sits
| Location | Must encode |
|---|---|
| Path segment | / (or it splits into two segments), ?, #, space, and anything non-ASCII |
| Query parameter key or value | &, =, #, + (if it should stay literal), space, and anything non-ASCII |
| Fragment | Same as a query value — # can't appear again, space and non-ASCII still need it |
Userinfo (user:pass@host) | :, @, and /, since any of those would be misread as ending the userinfo section early |
| Hostname | Not percent-encoded at all — non-ASCII hostnames use punycode (xn--) instead, a completely different mechanism |
Two characters worth calling out specifically
- Space. Never valid literally, anywhere in a URL.
%20is always correct; a query string will also accept+for it specifically, following the older form-encoding convention — see URL Encoding Explained for why that exception exists. - Non-ASCII characters (accented letters, CJK text, emoji) always need encoding — there's no context where they're left literal — and each one expands into multiple
%XXgroups, one per UTF-8 byte, not one group per character.
When in doubt
If you're encoding one value that's going into a URL — not building the whole URL by hand — encodeURIComponent (or your language's equivalent) is almost always the right call, precisely because it encodes everything structural and leaves the rest alone. It's rarely wrong to encode a character that didn't strictly need it; it's often a real bug to leave one unencoded that did.
Try it yourself
Encode or decode a value directly with URL Encode and URL Decode. To check whether a URL you've already got is missing encoding, has it doubled up, or has a malformed % sequence, run it through URL Inspector. All run entirely in your browser.