URL Encoding Chart: Percent-Encoding Cheat Sheet and URL Encoded Characters List

URL encoding replaces characters that would otherwise confuse a parser with a percent sign followed by their hex byte value. Which characters need it is not a fixed list — it depends on where in the URL the character appears, and the same character can be perfectly legal in one position and destructive in another. Here is the full table, plus the rules that make it usable.

URL encoding characters: the full table

Reserved characters carry structural meaning in a URL; the others in this table are not allowed literally at all. A reserved character must be encoded when it appears as data rather than syntax, and the rest are always encoded.

space  %20      &  %26      ;  %3B      [  %5B
  !    %21      '  %27      <  %3C      ]  %5D
  "    %22      (  %28      =  %3D      ^  %5E
  #    %23      )  %29      >  %3E      `  %60
  $    %24      *  %2A      ?  %3F      {  %7B
  %    %25      +  %2B      @  %40      |  %7C
  ,    %2C      /  %2F      \\  %5C      }  %7D
  :    %3A

Three of these break things far more often than the rest. & separates query parameters, = separates a key from its value, and # starts the fragment — which the browser never sends to the server at all. An unencoded # inside a value silently truncates everything after it, and because the request still succeeds with a shorter value, it is easy to misread as a server-side bug.

% deserves its own note. It introduces every escape sequence, so a literal percent sign must become %25. Miss it and the two characters that follow are read as a hex code — 100% followed by anything can decode into a completely different byte, or throw a malformed-URI error.

The characters you never encode

The unreserved set is safe in every part of a URL and should be left alone.

A-Z   a-z   0-9   -   _   .   ~

A hyphen never needs encoding. It is unreserved in RFC 3986 and legal anywhere in a URL — in the path, in a query key, in a value, in a fragment. If an encoder turns - into %2D, the result is still valid and decodes identically, but it is unnecessary and makes URLs harder to read and to match in logs.

The same applies to the period, the underscore and the tilde. The tilde is the one with history: older RFCs treated it as unsafe, so some legacy encoders still emit %7E. Both forms work, but current implementations should leave it alone.

This is why encodeURIComponent in JavaScript leaves - _ . ! ~ * ' ( ) untouched, and why server-side encoders sometimes differ on the last five. For anything involved in a signature — OAuth, AWS SigV4, a webhook HMAC — that difference is not cosmetic: two encoders producing different strings produce different signatures, and the request is rejected.

Space is the special case

A space has two valid encodings, and which one is correct depends entirely on context.

%20   in a path or query string (RFC 3986)
+     in an application/x-www-form-urlencoded body
%2B   a literal plus sign, in either context

This is the one place the two schemes genuinely conflict. In a form-encoded body, + means space, so a literal plus must be sent as %2B. Get it wrong and a timestamp such as 2026-09-28T10:00:00+02:00 arrives as 2026-09-28T10:00:00 02:00, which most date parsers reject or, worse, read as UTC.

Most server frameworks decode + as a space in query strings too, for historical compatibility. Using %20 everywhere avoids the ambiguity entirely and is always correct.

Non-ASCII, emoji and why strings get long

Anything outside ASCII is first encoded to UTF-8 bytes, and then each byte becomes its own %XX pair.

é       -> %C3%A9              2 bytes
ж       -> %D0%B6              2 bytes
中      -> %E4%B8%AD           3 bytes
😀      -> %F0%9F%98%80        4 bytes

So one character can legitimately expand to twelve characters of output. A Cyrillic or Chinese query string looks enormous once encoded, and that is correct rather than a bug.

The step that gets skipped is the UTF-8 conversion. Encoding byte-by-byte from a different code page produces sequences that decode to the wrong characters — the classic é in place of é. If accented characters survive one system and break in another, compare the raw percent-encoded bytes rather than the rendered text.

Context changes the rules

The same character needs different treatment depending on which part of the URL it lands in. This is the source of most encoding bugs that survive review.

https://host.com/path/seg?key=value#frag
                 ^^^^^^^^^ ^^^ ^^^^^ ^^^^
                 path      key value fragment

In a **path segment**, / is the separator, so a slash inside a value must be %2F. Be warned that many servers and proxies reject or normalise %2F in paths before your application sees it — nginx and Apache both do by default. Where you control the design, avoid slashes in path values rather than fighting the stack.

In a **query string**, encode each key and each value separately, then join them with & and =. Encoding the assembled string in one pass destroys the structure: the separators themselves get encoded and the server receives a single parameter with a very long name.

In a **fragment**, encoding is looser because the fragment never reaches the server. It still matters to client-side routers, which parse it the same way a server parses a query string.

Encoding correctly in code

Every language ships the right function; the mistake is almost always choosing the wrong one of a near-identical pair.

// encodeURI       — for a whole URL, preserves : / ? & = #
// encodeURIComponent — for one value, encodes them

encodeURI('https://a.com/x y?q=1&r=2');
// 'https://a.com/x%20y?q=1&r=2'   structure intact

encodeURIComponent('a&b=c');
// 'a%26b%3Dc'                     safe as a value

// Best: let the URL API assemble it
const u = new URL('https://a.com/search');
u.searchParams.set('q', 'a&b=c');
u.toString();  // '...?q=a%26b%3Dc'
# Python — quote() keeps '/' by default, quote_plus() does not
from urllib.parse import quote, quote_plus, urlencode

quote('a/b c')        # 'a/b%20c'   — '/' preserved for paths
quote('a/b c', safe='')  # 'a%2Fb%20c'
quote_plus('a b')     # 'a+b'       — for form bodies
urlencode({'q': 'a&b'})  # 'q=a%26b'  — builds the whole string
// PHP — rawurlencode is RFC 3986, urlencode is form-style
rawurlencode('a b');   // 'a%20b'   use for paths and query values
urlencode('a b');      // 'a+b'     use for form bodies
http_build_query(['q' => 'a&b']);  // 'q=a%26b'

The pattern repeats across languages: one function is RFC 3986 and gives %20, the other is form-style and gives +. Picking by name rather than by behaviour is how plus signs end up in URLs and spaces end up in form bodies.

Double-encoding is the other recurring bug. Outside data that really contains a percent sign, %25 should almost never appear in a URL; when it does, some layer encoded a value that was already encoded, and %20 has become %2520.

Context traps: ampersands, brackets, backslashes and quotes

A literal ampersand inside a value must be %26; otherwise it ends that value and starts another parameter. Do not confuse URL encoding with HTML escaping: an HTML document writes a query separator as &amp;, but the actual HTTP request contains &.

Square brackets are structural only around an IPv6 host. In a path or query value they are %5B and %5D, even though frameworks commonly accept keys such as filter[status]. Curly braces are %7B and %7D; literal braces in a request often reveal an unexpanded route template.

A backslash is %5C, not an alternative URL path separator. Browsers may normalise \ to / for HTTP URLs while a validator or server library does not. That parser disagreement can turn a value such as /\evil.example into an external redirect, so validate redirects by parsing the resolved URL and checking its origin—not with a string prefix.

A colon used as data is %3A, especially in a path or the first segment of a relative reference where it could look like a scheme. An apostrophe is %27. Encoding the quote is a transport rule, not protection from SQL injection: servers decode it before application code sees it, so database access still requires parameterised queries.

Frequently asked questions

Do I need to URL encode a dash?

No. The hyphen is an unreserved character in RFC 3986 and is legal anywhere in a URL — path, query key, value or fragment. Encoding it as `%2D` is valid and decodes identically, but it is unnecessary and makes URLs harder to read.

Which characters must always be encoded?

The reserved set when used as data rather than syntax: space, `!` `"` `#` `$` `%` `&` `'` `(` `)` `*` `+` `,` `/` `:` `;` `<` `=` `>` `?` `@` `[` `\` `]` `^` `` ` `` `{` `|` `}`. The three that cause the most damage are `&`, `=` and `#`, because they silently change how the URL is parsed rather than producing an error.

Should a space be %20 or +?

`%20` in a path or query string, `+` only inside an `application/x-www-form-urlencoded` body. A literal plus sign must be `%2B` in a form body, otherwise it decodes as a space, which is how a `+02:00` time-zone offset turns into ` 02:00`.

How do I encode a percent sign?

As `%25`. The percent sign starts every escape sequence, so a literal one must be encoded first. If it is not, the two characters after it are read as a hex code and either decode to the wrong byte or raise a malformed-URI error.

How do I encode square brackets in a URL?

As `%5B` and `%5D`; `encodeURIComponent` and `URLSearchParams` both do this. Brackets are structural only around an IPv6 host, such as `http://[::1]:8080/`. The WHATWG URL parser leaves literal brackets in a query untouched, which is why keys like `filter[status]` usually work unencoded.

Why does my accented character arrive as é?

The text was encoded from a non-UTF-8 code page. Percent-encoding operates on UTF-8 bytes, so `é` must become `%C3%A9`; encoding from Latin-1 gives `%E9`, which a UTF-8 decoder then renders as mojibake. Compare the raw percent-encoded bytes rather than the displayed characters.