Redirects vs Rewrites: A Critical Distinction
A redirect tells the client's browser to make a brand new request to a different URL, using the return directive with a 301 (permanent) or 302 (temporary) status code, for example return 301 https://$host$request_uri;. A rewrite, done with the rewrite directive, changes the URI internally on the server without the client ever knowing, and Nginx continues processing the request against the new URI. Confusing the two is a common mistake: using rewrite when you meant a return forces Nginx to run regex processing on every request unnecessarily, and can create loops if the rewritten URI matches the same rewrite rule again.
Cricket analogy: It's like the difference between an umpire sending a batsman back to the pavilion to walk in fresh through a different gate (redirect, visible to everyone) versus a scorer quietly correcting the scorecard internally without announcing anything (rewrite, invisible to the crowd).
Writing Safe rewrite Rules
The rewrite directive syntax is rewrite regex replacement [flag];, evaluated within a server or location context. The last flag stops processing the current rewrite set and re-searches location blocks with the new URI, while break stops rewrite processing but stays within the current location. A frequent production incident is a rewrite rule whose replacement can match its own regex, creating an infinite internal loop that Nginx eventually kills after 10 iterations with a 500 error — always test rewrite rules against edge cases like trailing slashes and query strings before deploying.
Cricket analogy: It's like a captain's fielding instruction that says 'if the ball goes to deep cover, immediately re-assess the whole field' (last, full re-evaluation) versus 'just shift this one fielder slightly and carry on with the same plan' (break, stay in current context).
Preserving Method, Query Strings, and Loops
A return 301 preserves the original HTTP method poorly — browsers convert POST to GET on a 301 in older implementations, so a 307 or 308 status should be used when the method must be preserved across a redirect. Query strings are appended automatically by return 301 https://$host$request_uri; since $request_uri includes them, but a manually constructed rewrite target must explicitly append $args or the query string is silently dropped, breaking anything relying on tracking parameters or pagination.
Cricket analogy: It's like a rain-delay rule that resets the match format entirely (dropping context, like a 301 losing POST data) versus a Duckworth-Lewis recalculation that carefully preserves the original match context (like a 308 preserving method and body).
server {
listen 80;
server_name example.com www.example.com;
# Permanent redirect to HTTPS canonical host, preserving path + query string
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
# Internal rewrite: old blog paths to new structure, without a client-visible redirect
rewrite ^/blog/([0-9]+)/(.*)$ /articles/$2?id=$1 last;
# Explicit redirect for a deprecated marketing path, preserving query args
location /promo {
return 302 /offers?$args;
}
}Prefer return over rewrite whenever you're just issuing a redirect and not changing internal routing — return is evaluated immediately and skips Nginx's regex engine, making it both clearer to read and measurably cheaper on high-traffic servers.
A rewrite rule whose output can match its own input regex creates an internal loop. Nginx caps internal redirects at 10 and then returns a 500 Internal Server Error — always trace a new rewrite rule with rewrite_log on; in a staging environment before shipping it to production.
- return issues a client-visible HTTP redirect (301/302/307/308); rewrite changes the URI internally on the server.
- 301 is permanent and cacheable by browsers; 302 is temporary and should be revalidated on every request.
- 307 and 308 preserve the original HTTP method and body, unlike 301/302 in some legacy clients.
- The last flag re-searches location blocks with the new URI; break stays within the current location.
- $request_uri includes the query string automatically; manual rewrite targets must append $args explicitly.
- A rewrite rule that can match its own output creates an internal loop, capped at 10 iterations before a 500 error.
- Prefer return for simple redirects — it skips regex evaluation and is both faster and easier to audit.
Practice what you learned
1. What is the key difference between `return 301 ...` and `rewrite ... last;`?
2. Which HTTP status codes preserve the original request method and body across a redirect?
3. What does the `last` flag on a rewrite directive do?
4. Why must a manually constructed rewrite replacement explicitly include $args?
5. What happens when a rewrite rule's output can match its own input regex repeatedly?
Was this page helpful?
You May Also Like
Locations and Matching Rules
How Nginx's location block matching algorithm actually works — prefix, exact, and regex modifiers, and the precedence order that trips up most misconfigurations.
Nginx Variables
How Nginx's dollar-sign variables are lazily computed per request, the difference between commonly confused ones like $uri and $request_uri, and how map builds custom derived variables cleanly.
Server Blocks Explained
How Nginx uses server blocks to host multiple independent sites on a single instance, and how listen and server_name determine which block answers a request.
Related Reading
Related Study Notes in DevOps
Browse all study notesAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics