Mod_rewrite breaks in predictable ways. After years of reading server logs and fixing redirect loops, I've seen the same mistakes over and over. Most of them are silent—Apache won't throw an error, your site just won't redirect the way you expect.
This is the catalogue of those mistakes and the exact fix for each one.
Mistake 1: RewriteEngine is off or missing
The most common apache mod_rewrite mistake is forgetting to turn the engine on. Without RewriteEngine On, every rule in your .htaccess is ignored. No error, no warning.
Check the top of your .htaccess file:
RewriteEngine On
It only needs to appear once per context. If you're working in a virtual host block in the main Apache config, the same rule applies. Turn it on before any RewriteRule or RewriteCond directive.
In support tickets I handled, this was the culprit maybe 30% of the time when someone said "my redirects aren't working." They'd copied a RewriteRule from Stack Overflow and pasted it into .htaccess without the engine directive.
Mistake 2: Wrong flag syntax
Flags control how Apache processes a rule. The syntax is square brackets at the end of the line, comma-separated, no spaces.
Wrong:
RewriteRule ^old-page$ /new-page [R=301] [L]
RewriteRule ^old-page$ /new-page [R=301, L]
Right:
RewriteRule ^old-page$ /new-page [R=301,L]
Apache will silently ignore malformed flags or throw a 500 error depending on the version. The error log will say something like "RewriteRule: bad flag delimiters" but most people don't check logs first.
Common flags you'll use: L (last rule, stop processing), R=301 (external redirect with 301 status), NC (case-insensitive match), QSA (append query string).
Mistake 3: Forgetting the leading slash in substitutions
When you write a RewriteRule in .htaccess, the pattern matches against the URL path without the leading slash. But the substitution often needs one.
Wrong:
RewriteRule ^blog/(.*)$ blog/index.php?slug=$1 [L]
If the substitution is a relative path, Apache will sometimes interpret it incorrectly or append it to the current directory path in unexpected ways. Use an absolute path from the document root:
RewriteRule ^blog/(.*)$ /blog/index.php?slug=$1 [L]
Or use a full URL if you're doing an external redirect:
RewriteRule ^old-blog/(.*)$ https://example.com/blog/$1 [R=301,L]
Mistake 4: Rule order causes early exit
The [L] flag stops processing. If you put a catch-all rule with [L] before a more specific one, the specific rule never runs.
Wrong order:
RewriteRule ^(.*)$ /index.php?path=$1 [L]
RewriteRule ^api/(.*)$ /api.php?endpoint=$1 [L]
The second rule is unreachable. Every request matches the first pattern, and [L] stops processing.
Right order:
RewriteRule ^api/(.*)$ /api.php?endpoint=$1 [L]
RewriteRule ^(.*)$ /index.php?path=$1 [L]
Put specific patterns first, generic catch-alls last. This is one of those mistakes that's obvious in hindsight but brutal to debug when you're staring at a .htaccess file with 50 lines of rules.
Mistake 5: RewriteCond applies only to the next RewriteRule
A RewriteCond directive adds a condition to the rule that immediately follows it. If you write two conditions and then two rules, the conditions only affect the first rule.
Wrong:
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
RewriteRule ^old$ /new [R=301,L]
The second RewriteRule has no condition. It will always execute if the first rule doesn't match, regardless of HTTPS status.
If you need the same condition for multiple rules, repeat it:
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
RewriteCond %{HTTPS} off
RewriteRule ^old$ /new [R=301,L]
Or restructure your logic so one rule handles it. In most cases the first approach—redirecting all HTTP to HTTPS in a single catch-all—is cleaner.
Mistake 6: Not escaping dots in regex patterns
The pattern in a RewriteRule is a regular expression. A dot matches any character. If you're trying to match a literal dot (like in a file extension), escape it.
Wrong:
RewriteRule ^(.+).html$ /$1 [R=301,L]
This matches page.html, but it also matches pagexhtml or page/html. The dot is a wildcard.
Right:
RewriteRule ^(.+)\.html$ /$1 [R=301,L]
Same goes for other regex special characters: ^, $, *, +, ?, [, ], (, ), |. If you want the literal character, escape it with a backslash.
So what if the rule looks right but still doesn't work?
Check whether AllowOverride is set correctly in your Apache config. If you're editing .htaccess but the server config has AllowOverride None, Apache ignores the .htaccess file entirely.
In a cPanel environment, this is usually fine—cPanel sets AllowOverride All by default for user directories. On a VPS or dedicated server you manage yourself, you might need to edit the virtual host block:
<Directory /var/www/html>
AllowOverride All
</Directory>
Restart Apache after changing the main config:
sudo systemctl restart httpd
# or on Debian/Ubuntu:
sudo systemctl restart apache2
No restart is needed for .htaccess changes—those are read on every request.
Mistake 7: Query strings aren't matched by the pattern
The RewriteRule pattern matches only the URL path. Query strings (everything after ?) are not included. If you need to match or manipulate query strings, use %{QUERY_STRING} in a RewriteCond.
Example: redirect page.php?id=123 to page/123.
Wrong:
RewriteRule ^page\.php\?id=(.*)$ /page/$1 [R=301,L]
This never matches because the pattern doesn't see the query string.
Right:
RewriteCond %{QUERY_STRING} ^id=(.*)$
RewriteRule ^page\.php$ /page/%1 [R=301,L]
Notice %1 instead of $1. Backreferences from RewriteCond use %N, backreferences from RewriteRule use $N. Mixing them up is another common mistake.
Mistake 8: Redirect loops because the target matches the pattern
If your RewriteRule redirects to a path that itself matches the pattern, you get a loop. Browsers will stop after a few iterations and show an error.
Wrong:
RewriteRule ^(.*)$ /index.php [L]
This matches every request, including /index.php, and rewrites it to /index.php again. Infinite loop.
Fix it by excluding the target file:
RewriteCond %{REQUEST_URI} !^/index\.php$
RewriteRule ^(.*)$ /index.php [L]
Or use RewriteCond %{REQUEST_FILENAME} !-f to skip the rule if the request already points to an existing file:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]
This is the pattern most PHP frameworks use. It says: if the request isn't an actual file or directory, send it to the front controller.
Mistake 9: Not checking the error log
Apache logs every rewrite operation if you enable the rewrite log, and errors always go to the error log. When a rule doesn't work, check the log first.
Error log location on most cPanel servers:
/usr/local/apache/logs/error_log
On a VPS with a standard Apache install:
/var/log/httpd/error_log
# or
/var/log/apache2/error.log
Tail it in real-time while you test:
tail -f /var/log/httpd/error_log
You'll see syntax errors, flag problems, and sometimes even helpful messages like "RewriteRule: maximum number of internal redirects reached" which tells you exactly that you have a loop.
For detailed rewrite tracing, add this to your virtual host or .htaccess (Apache 2.4+):
LogLevel alert rewrite:trace3
This dumps every step of the rewrite process to the error log. It's verbose, but when a rule isn't doing what you expect, the trace shows you exactly which conditions matched and which didn't. Turn it off after debugging—production logs will bloat fast.
What to check first
When mod_rewrite isn't working, run through this list:
- Is
RewriteEngine Onat the top of your .htaccess? - Does your flag syntax use square brackets, commas, no spaces?
- Are specific rules before generic catch-alls?
- Did you escape dots and other regex special characters?
- If you're matching query strings, did you use
RewriteCond %{QUERY_STRING}? - Does the substitution have a leading slash for relative paths?
- Is
AllowOverride Allset in the Apache config? - Is there a redirect loop (target matches the pattern again)?
- What does the error log say?
Most rewrite problems come down to one of these nine mistakes. Fix the syntax, check rule order, read the logs. Mod_rewrite is powerful but unforgiving—it does exactly what you tell it to, not what you meant.
Frequently asked questions
Can I test RewriteRule patterns without restarting Apache?
Yes. Changes to .htaccess are live immediately. For testing patterns, use an online regex tester or the curl -I command to see response headers without rendering the page in a browser.
Why does my redirect work in a browser but not with curl?
Browsers cache 301 redirects aggressively. Clear your cache or test in incognito mode. Curl doesn't cache, so if curl shows the redirect and your browser doesn't, it's cached.
What's the difference between R and R=301?
Plain [R] sends a 302 temporary redirect. [R=301] is a permanent redirect. Search engines treat them differently—301 passes link equity, 302 doesn't. Use 301 for permanent moves.
Do I need to restart Apache after editing .htaccess?
No. Apache reads .htaccess on every request. Changes are instant. You only need to restart if you edit the main config files in /etc/httpd/ or /etc/apache2/.
Can I use mod_rewrite to redirect HTTP to HTTPS?
Yes. This is one of the most common uses:
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
Put it at the top of .htaccess so it runs before other rules.
