You already know the basics of mod_rewrite—redirecting www to non-www, forcing HTTPS, and cleaning up URLs. This guide skips the beginner syntax and dives into production-grade optimization, edge cases that break under load, and techniques that separate competent Apache admins from experts.
Understanding Rewrite Engine Performance Overhead
Every RewriteRule evaluation costs CPU cycles. When mod_rewrite processes a request, it walks through every active rule in the current context, testing conditions and applying transformations. Under high traffic, inefficient rulesets become bottlenecks.
Rule Processing Order Matters
Apache evaluates rewrite rules top-to-bottom within each context (server config, virtual host, .htaccess). Place your most-frequently-matched rules first. If 80% of your traffic hits a single pattern, test for it early and use the [L] flag to stop processing.
# Good: common case first
RewriteRule ^api/ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /index.php [L]
# Bad: forces every API request through file existence check
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /index.php [L]
RewriteRule ^api/ - [L]
Context Inheritance and Duplication
Rules in server config are inherited by virtual hosts, which are inherited by directory contexts, which are inherited by .htaccess files. Each context re-evaluates its own rules plus inherited ones. This creates multiplicative overhead.
Move rules from .htaccess to virtual host configuration whenever possible. Directory-level .htaccess files are parsed on every request, while server config is compiled once at startup. The performance difference under load is substantial.
# In virtual host config (preferred)
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/example
RewriteEngine On
RewriteRule ^old-path/(.*)$ /new-path/$1 [R=301,L]
<Directory /var/www/example>
AllowOverride None
</Directory>
</VirtualHost>
Set AllowOverride None in directory blocks to prevent .htaccess parsing entirely when you control all rules centrally.
RewriteMap: The Secret Weapon
RewriteMap lets you externalize complex lookup logic instead of sprawling conditional chains. It supports multiple backend types, each with different performance characteristics.
Text File Maps with Caching
The txt map type loads a key-value file into memory at startup. It is extremely fast for static lookups but requires a graceful reload to update.
RewriteMap redirects txt:/etc/apache2/redirects.txt
RewriteRule ^(.*)$ ${redirects:$1|/} [R=301,L]
redirects.txt:
old-product new-product
legacy-page archive/legacy-page
promo-2025 products
Keys must match exactly—no regex support. For case-insensitive lookups, use the int:tolower map function first:
RewriteMap lowercase int:tolower
RewriteMap redirects txt:/etc/apache2/redirects.txt
RewriteRule ^(.*)$ ${redirects:${lowercase:$1}|/} [R=301,L]
DBM Maps for Large Datasets
When your redirect map exceeds a few thousand entries, convert to DBM format for faster lookups:
httpcxt /etc/apache2/redirects.txt /etc/apache2/redirects.db
RewriteMap redirects dbm:/etc/apache2/redirects.db
DBM maps use hash tables internally and scale better than linear text file scans. Benchmark your specific dataset—the crossover point depends on key distribution and memory.
Program Maps for Dynamic Logic
The prg map type pipes lookups to an external script via stdin/stdout. The script must read one key per line and write one value per line, running indefinitely.
RewriteMap geoip prg:/usr/local/bin/geoip-lookup.sh
RewriteRule ^(.*)$ - [E=COUNTRY:${geoip:%{REMOTE_ADDR}}]
Critical: Program maps launch once per Apache child process. A poorly-written script that blocks or leaks memory will multiply across all workers. Always test under load with realistic concurrency.
Use program maps sparingly. For most dynamic logic, reverse proxy to a dedicated microservice instead of embedding it in the rewrite layer.
Optimizing Regular Expressions
Non-Capturing Groups
If you do not need to backreference a group, use non-capturing syntax (?:...) instead of (...). This reduces memory allocation and makes your intent explicit.
# Capturing (slower)
RewriteRule ^(blog|news|articles)/([0-9]+)$ /post.php?id=$2 [L]
# Non-capturing (faster)
RewriteRule ^(?:blog|news|articles)/([0-9]+)$ /post.php?id=$1 [L]
Anchors and Specificity
Always anchor patterns with ^ and $ when appropriate. Unanchored patterns allow the regex engine to scan for matches anywhere in the string, wasting cycles.
# Bad: matches "example" anywhere in URI
RewriteRule example /other [L]
# Good: matches only URIs starting with /example
RewriteRule ^/example /other [L]
Avoid Greedy Quantifiers in High-Traffic Rules
Greedy quantifiers like .* backtrack extensively when matches fail. Use possessive or atomic groups to eliminate backtracking:
# Greedy (backtracks on mismatch)
RewriteRule ^/files/(.*)\.(jpg|png)$ /images/$1.$2 [L]
# Possessive (fails fast)
RewriteRule ^/files/(.*+)\.(jpg|png)$ /images/$1.$2 [L]
Apache uses PCRE for regex. Benchmark complex patterns with ab or realistic traffic replay tools.
Conditional Logic Edge Cases
Multiple Conditions with Implicit AND
RewriteCond directives stack with implicit AND logic. Every condition must match for the subsequent RewriteRule to fire.
RewriteCond %{HTTP_HOST} ^www\.
RewriteCond %{HTTPS} on
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
Both conditions must be true. To express OR logic, use the [OR] flag:
RewriteCond %{HTTP_USER_AGENT} bot [NC,OR]
RewriteCond %{HTTP_USER_AGENT} crawler [NC]
RewriteRule ^(.*)$ /rate-limited.html [L]
Testing for File and Directory Existence
The -f and -d tests trigger filesystem stat calls. Under heavy load, this becomes an I/O bottleneck. Cache negative lookups whenever possible by testing for existence patterns first.
# Avoid stat calls for known static paths
RewriteRule ^(?:css|js|images)/ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]
Consider enabling a filesystem cache layer or moving static assets to a separate CDN origin to reduce filesystem pressure.
Variable Scope and Backreferences
RewriteCond backreferences use %1, %2, etc., while RewriteRule backreferences use $1, $2, etc. Mixing them is a common mistake.
RewriteCond %{HTTP_HOST} ^(.+)\.example\.com$
RewriteRule ^(.*)$ /sites/%1/$1 [L]
Here %1 captures the subdomain from the condition, and $1 captures the URI path from the rule.
Performance Tuning in Production
Reduce RewriteLog Overhead
The legacy RewriteLog directive (now LogLevel rewrite:trace) writes verbose logs on every rewrite decision. It is invaluable for debugging but catastrophic for performance under load.
Only enable rewrite tracing in isolated testing. In production, log to the error log at warn or error level.
# Development
LogLevel rewrite:trace3
# Production
LogLevel rewrite:warn
Use [PT] for ProxyPass Integration
When rewriting URIs that will be handled by ProxyPass, use the [PT] (pass-through) flag to hand off the rewritten URI to the proxy handler without additional rule processing.
RewriteRule ^/api/(.*)$ /backend/api/$1 [PT]
ProxyPass /backend http://127.0.0.1:8080
Without [PT], the rewritten URI might not match your ProxyPass directive, causing unexpected 404s.
Environment Variable Overhead
Setting environment variables with [E=VAR:value] is cheap, but excessive variable manipulation adds overhead. If you set environment variables only for logging or conditional headers, question whether you truly need them.
# Useful for logging
RewriteRule ^/admin/ - [E=ADMIN_ACCESS:1]
CustomLog logs/access.log combined env=ADMIN_ACCESS
# Wasteful if never used
RewriteRule ^(.*)$ - [E=FULL_URI:$1]
Edge Cases and Gotchas
Query String Handling
RewriteRule patterns match only the URI path, not the query string. To test or manipulate query strings, use %{QUERY_STRING} in conditions.
RewriteCond %{QUERY_STRING} ^id=([0-9]+)$
RewriteRule ^/product$ /product/%1? [R=301,L]
The trailing ? in the substitution discards the original query string. Omit it to append the original query string.
Encoded Slashes and Path Segments
By default, Apache decodes URL-encoded characters before mod_rewrite sees them. Encoded slashes %2F become /, potentially breaking your patterns. To preserve encoded slashes, set:
AllowEncodedSlashes NoDecode
Without NoDecode, requests with %2F may return 404 even if your rewrite logic expects them.
Infinite Loop Detection
Carelessly written rules can loop indefinitely. Apache detects internal redirect loops and aborts with a 500 error, but not before consuming resources. Always include conditions that prevent re-matching:
# Bad: loops forever
RewriteRule ^(.*)$ /prefix/$1 [L]
# Good: stops after first match
RewriteCond %{REQUEST_URI} !^/prefix/
RewriteRule ^(.*)$ /prefix/$1 [L]
The [L] flag stops further rule processing in the current context but does not prevent subsequent per-directory or .htaccess passes. Use [END] (Apache 2.4+) to terminate all rewrite processing immediately.
SSL/TLS Termination and HTTPS Detection
Behind a load balancer or reverse proxy, %{HTTPS} may not reflect the client's connection. Check the forwarded protocol header instead:
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
Confirm which header your proxy sets—common variations include X-Forwarded-SSL, X-Forwarded-Proto, and CloudFront-Forwarded-Proto.
Security Considerations
Input Validation and Injection
Never trust captured groups from user input without validation. Malicious URIs can inject path traversal sequences or exploit downstream handlers.
# Dangerous: allows path traversal
RewriteRule ^/files/(.*)$ /var/www/files/$1 [L]
# Safer: restrict to known safe patterns
RewriteRule ^/files/([a-zA-Z0-9_-]+\.[a-z]{3,4})$ /var/www/files/$1 [L]
If your rewrite logic proxies to an application, ensure the application also validates input. Defense in depth.
Open Redirect Vulnerabilities
Using %{QUERY_STRING} or other user-controlled input in redirect targets creates open redirect risks:
# Vulnerable
RewriteCond %{QUERY_STRING} url=(.+)
RewriteRule ^/bounce$ %1? [R=302,L]
An attacker can craft https://yoursite.com/bounce?url=https://evil.com to redirect users off your domain. Whitelist allowed targets or validate the scheme and host.
Debugging Complex Rules
Isolate and Test Incrementally
When a ruleset misbehaves, comment out all rules and re-enable them one at a time. Use curl -v to inspect raw HTTP transactions:
curl -v -H "Host: example.com" http://127.0.0.1/test-path
Check the Location header in redirects and the final Request-URI in proxied or internal rewrites.
Temporary Test Flags
Replace [R=301,L] with [R=302,L] during testing to avoid browser caching of permanent redirects. Confirm behavior before switching to 301.
Use the [E=DEBUG:1] flag to set environment variables and log them:
RewriteRule ^/test - [E=DEBUG:matched,L]
CustomLog logs/debug.log "%{DEBUG}e %r"
Test with RewriteCond Negation
If a condition should match but does not, test its negation to confirm the variable is being evaluated:
RewriteCond %{HTTP_HOST} !^example\.com$
RewriteRule ^(.*)$ - [R=410,L]
If all requests return 410, the original positive condition was never true. Check for typos, case sensitivity, or regex escaping errors.
Conclusion
Advanced mod_rewrite work is about efficiency, precision, and understanding the edge cases that emerge under production load. Optimize for your most common paths, minimize filesystem checks, and externalize complex logic with RewriteMap. Test rule order, avoid regex backtracking, and always consider the security implications of user-controlled input in rewrites. The difference between adequate and expert-level mod_rewrite configuration becomes visible when traffic scales—invest the time to benchmark, profile, and tune your rulesets before problems emerge in production.
