Apache mod_rewrite is a regex-based engine that changes or redirects URLs before Apache serves them. It powers pretty permalinks, HTTPS redirects and countless .htaccess tricks, and it is also the source of many redirect loops. This guide explains how mod_rewrite actually evaluates rules so you can write and debug them with confidence. The same syntax works on LiteSpeed.
Before you start
- mod_rewrite enabled (
a2enmod rewrite) andAllowOverride FileInfo(or All) if you use.htaccess. - Basic regular expressions:
^,$,.,(.*),[a-z]+. - For ready-made recipes see friendly URLs with .htaccess.
Anatomy of a RewriteRule
RewriteEngine On
RewriteRule ^products/([0-9]+)/?$ product.php?id=$1 [L,QSA]- Pattern
^products/([0-9]+)/?$is matched against the URL path. - Substitution
product.php?id=$1;$1is the first captured group. - Flags in square brackets change behaviour.
What the pattern is matched against
This is the number one source of confusion:
- In a vhost or server config, the pattern sees the full path with a leading slash:
/products/42. - In .htaccess, the directory prefix is stripped: in the document root it sees
products/42with no leading slash. - The query string is never part of the pattern. Match it with
RewriteCond %{QUERY_STRING}.
A pattern like ^/products therefore works in a vhost but never matches in .htaccess.
RewriteCond: conditions
Conditions go directly above the rule they guard. All must be true (AND) unless you add [OR].
RewriteCond %{HTTPS} off
RewriteCond %{HTTP_HOST} ^(?:www\.)?(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]%1 refers to a group captured in the last matching RewriteCond; $1 refers to the RewriteRule pattern.
Useful server variables
| Variable | Contains |
|---|---|
%{HTTP_HOST} | Requested domain |
%{REQUEST_URI} | Path, with leading slash, no query string |
%{QUERY_STRING} | Everything after ? |
%{HTTPS} | on or off |
%{REQUEST_FILENAME} | Mapped file path on disk |
%{REMOTE_ADDR} | Client IP |
%{HTTP_USER_AGENT} | Browser string |
Test operators: -f (is a file), -d (is a directory), ! to negate, = for an exact string.
The flags you need
| Flag | Effect |
|---|---|
L | Last: stop processing this round of rules |
END | Stop and do not start another round (stops .htaccess loops) |
R=301 / R=302 | External redirect with that status |
NC | Case-insensitive match |
QSA | Append the original query string to the new one |
QSD | Discard the original query string |
NE | Don't escape special characters such as # |
F / G | Return 403 Forbidden / 410 Gone |
P | Proxy the request (needs mod_proxy) |
Why L does not always stop in .htaccess
In .htaccess, an internal rewrite to a new path triggers a fresh pass through the same .htaccess file. [L] ends only the current pass. If the new URL matches again you get a loop or a 500 "Request exceeded the limit of 10 internal redirects". Fixes:
- Add
RewriteCond %{REQUEST_FILENAME} !-fand!-dso real files are left alone. - Use
[END]instead of[L](Apache 2.4+, supported by LiteSpeed).
The front controller pattern
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]Every URL that is not an existing file or folder goes to index.php, which routes it. This is how WordPress, Laravel and most PHP frameworks work.
Prefer simpler tools when you can
- A single fixed redirect:
Redirect 301 /old /new(mod_alias) is clearer. - In a vhost you control, rules there are faster than .htaccess, which is read on every request.
- Use
<If>expressions for simple header logic.
Debug rewrite rules
- Test redirects without the browser cache:
curl -sI https://example.com/old. - Use 302 while testing; browsers cache 301s aggressively.
- On your own Apache, enable tracing temporarily:
LogLevel alert rewrite:trace3in the vhost, then read the error log. Turn it off afterwards.
FAQ: Apache mod_rewrite
Does order of rules matter?
Yes. Rules run top to bottom; put specific rules before general catch-alls.
Why does my rule work in the vhost but not in .htaccess?
The leading slash. Remove / from the start of the pattern in .htaccess.
Does mod_rewrite work on LiteSpeed?
Yes, LiteSpeed Enterprise parses the same rules, including in .htaccess.
