I’ll be honest — mod_rewrite intimidated me for a long time. The regex-heavy syntax looked like line noise the first few times I tried to read someone else’s rewrite rules. But once I sat down and actually learned the logic behind it, it became one of the most powerful tools in my Apache toolkit, letting me clean up ugly URLs, enforce HTTPS, set up redirects, and build entire routing systems for single-page applications.
What mod_rewrite Actually Does
mod_rewrite is Apache’s URL rewriting engine. It lets you intercept incoming requests and transform them based on pattern matching (regular expressions), before Apache decides how to actually serve the request. This lets you:
- Turn ugly query-string URLs like
/product.php?id=42into clean URLs like/products/42. - Redirect old URLs to new ones after a site restructure.
- Force HTTPS or force the
www(or non-www) version of your domain. - Route all requests through a single front controller file, which is how most modern PHP frameworks and CMS platforms (WordPress, Laravel, Symfony) handle routing.
- Block access based on request patterns, user agents, or referrers.
Prerequisites
- Apache installed with
mod_rewriteavailable. - Root/sudo access, or at least
AllowOverride Allpermission if you’re working through.htaccess. - Basic familiarity with regular expressions (I’ll explain the ones I use as we go).
Step 1: Enable mod_rewrite
sudo a2enmod rewrite
sudo systemctl restart apache2
On CentOS/RHEL, mod_rewrite is usually enabled by default, but verify with:
httpd -M | grep rewrite
Step 2: Allow Overrides (If Using .htaccess)
If you plan to place rewrite rules in .htaccess files rather than directly in your virtual host configuration, make sure AllowOverride All is set for your document root:
<Directory /var/www/example.com/public_html>
AllowOverride All
</Directory>
I generally prefer putting rewrite rules directly in the virtual host file when I control the server, since .htaccess files add a small performance overhead (Apache has to check for them on every request). But .htaccess is unavoidable in shared hosting environments where you don’t have access to the main config.
Step 3: Understand the Core Directives
The three directives you’ll use constantly:
- RewriteEngine On: Turns on the rewrite engine for this context.
- RewriteCond: Defines a condition that must be true for the following
RewriteRuleto apply. - RewriteRule: The actual pattern-matching and replacement rule.
A basic rule looks like:
RewriteEngine On
RewriteRule ^old-page$ /new-page [R=301,L]
This says: if the request path matches exactly old-page, redirect (R=301, a permanent redirect) to /new-page, and stop processing further rules (L, meaning “last”).
Step 4: Common Real-World Rewrite Recipes
Force HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Force www
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Force non-www (the opposite)
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1%{REQUEST_URI} [L,R=301]
Clean URLs (Query String to Path)
Turning /product.php?id=42 into /products/42:
RewriteEngine On
RewriteRule ^products/([0-9]+)/?$ product.php?id=$1 [L,QSA]
Here, ([0-9]+) captures one or more digits, and $1 refers back to that captured group in the target.
Removing .php or .html Extensions
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.*)$ $1.php [L]
This checks if a .php file matching the request exists, and if so, serves it without requiring the visitor to type the extension.
Front Controller Pattern (Used by Most Frameworks)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
This routes every request that doesn’t match an actual file or directory through index.php, letting the application’s internal router handle it. This is exactly how WordPress’s permalink system and most PHP frameworks work under the hood.
Step 5: Understanding Rewrite Flags
Flags (the bracketed part at the end of a rule) control behavior:
- [L] — Last rule; stop processing further rewrite rules if this one matches.
- [R=301] or [R=302] — Issue an HTTP redirect (permanent or temporary) rather than an internal rewrite.
- [NC] — Case-insensitive matching.
- [QSA] — Query String Append; preserves existing query string parameters when rewriting.
- [P] — Proxy the request (requires
mod_proxy), commonly used for WebSocket rewriting as covered in my mod_proxy post. - [F] — Forbidden; return a 403 error for matching requests.
Step 6: Testing Your Rules Safely
Always test syntax before restarting in production:
sudo apache2ctl configtest
For debugging why a rule isn’t matching as expected, enable rewrite logging temporarily (Apache 2.4+ uses LogLevel with a module-specific trace level rather than the old RewriteLog directive):
LogLevel alert rewrite:trace3
Check your error log after making a request, and you’ll see a detailed trace of each condition and rule being evaluated. Remember to turn this back down to a normal log level once you’re done debugging — trace-level logging is verbose and will bloat your logs quickly.
Real-World Use Cases
- SEO-friendly URLs: Converting dynamic query-string URLs into clean, keyword-rich paths.
- Site migrations: Redirecting old URL structures to new ones after a redesign, preserving SEO value with proper 301 redirects.
- Single-page application routing: Sending all non-file requests to
index.htmlso client-side JavaScript routers can take over. - Canonical domain enforcement: Ensuring all traffic consistently uses
https://www.example.comrather than a mix of variations that could dilute SEO rankings. - Bot and scraper blocking: Rejecting requests based on suspicious user-agent strings or referrer patterns.
Troubleshooting Common Issues
Problem: “Too many redirects” errors. Usually caused by a rewrite rule that keeps matching its own output. Double-check your RewriteCond — for instance, forcing HTTPS without first checking %{HTTPS} off can create an infinite loop.
Problem: Rules work in the main config but not in .htaccess (or vice versa). Remember that in .htaccess context, the leading slash behavior and RewriteBase requirements can differ from virtual host context. Add RewriteBase / near the top of your .htaccess file if paths aren’t resolving correctly.
Problem: Query string parameters disappear after rewriting. Add the [QSA] flag to append the original query string to the rewritten URL instead of discarding it.
Problem: Rules seem to be ignored entirely. Confirm mod_rewrite is enabled (apache2ctl -M | grep rewrite) and that AllowOverride All is set if you’re using .htaccess.
Common Mistakes to Avoid
- Writing overly broad regex patterns that unintentionally match unrelated URLs.
- Forgetting the
[L]flag, causing Apache to keep evaluating subsequent rules after a match, sometimes producing unexpected double-rewrites. - Using
R=302(temporary redirect) for what should be a permanent URL change, hurting SEO consolidation. - Leaving
rewrite:trace3debug logging enabled in production, generating excessive log volume. - Not testing rewrite rules against edge cases like URLs with trailing slashes, special characters, or query strings.
Security Best Practices
- Use
mod_rewriteto block access to sensitive files (.env,.git, config files) that shouldn’t be publicly accessible:
RewriteRule ^\.env$ - [F,L]
RewriteRule ^\.git/ - [F,L]
- Be cautious with rewrite rules based on user-supplied input (like query parameters) that get passed into file paths — this can open the door to path traversal vulnerabilities if not carefully validated.
- Regularly review your rewrite rules during security audits, since overly permissive rules can inadvertently expose internal application logic or files.
Performance Optimization Tips
- Prefer virtual host-level rewrite rules over
.htaccesswhere you have access to do so — Apache has to check for.htaccessfiles in every parent directory on every request, adding overhead. - Keep regular expressions as specific as possible; overly broad patterns with excessive backtracking can slow down request processing under load.
- Combine
mod_rewriteredirects with proper caching headers so repeated redirect requests (from bots, old bookmarks) don’t add unnecessary load. - Consolidate related rules where possible rather than stacking many small, overlapping rewrite blocks.
Frequently Asked Questions
What’s the difference between RewriteRule and Redirect (from mod_alias)? Redirect and RedirectMatch handle simple path redirection without complex conditions. mod_rewrite is more powerful, supporting conditional logic, capture groups, and internal (non-redirected) rewrites.
Do I need mod_rewrite if I’m using a modern framework like Laravel? Yes, typically — most PHP frameworks rely on the front controller pattern, which requires a rewrite rule to route all requests through index.php.
Can mod_rewrite rules go in the httpd.conf file directly? Yes, either inside a <VirtualHost> block or a <Directory> block, which is generally more performant than using .htaccess.
How do I redirect an entire old domain to a new one while preserving paths?
RewriteEngine On
RewriteCond %{HTTP_HOST} ^olddomain\.com$ [NC]
RewriteRule ^(.*)$ https://newdomain.com/$1 [R=301,L]
Summary and Key Takeaways
mod_rewrite looks intimidating at first because of its regex-heavy syntax, but the underlying logic is straightforward once you’ve written a handful of rules: match a pattern, optionally check conditions, then rewrite or redirect.
Key points to remember:
- Enable
mod_rewriteand ensureAllowOverride Allif working through.htaccess. - Use
RewriteCondto add conditions before aRewriteRuleapplies. - Learn the common flags —
[L],[R=301],[NC],[QSA]— since you’ll use them constantly. - Test changes with
apache2ctl configtestbefore restarting, and userewrite:trace3logging temporarily when debugging. - Prefer virtual host-level rules over
.htaccessfor performance when you have the access to do so.
Once it clicks, mod_rewrite becomes one of those tools you reach for constantly — for SEO cleanup, migrations, security hardening, and framework routing alike.