URL redirects seem like they should be trivial, and most of the time they are — until you accidentally create a redirect loop at 2 AM during a domain migration and watch your site go completely unreachable while your browser cheerfully reports “too many redirects.” I’ve done this. More than once. This guide is everything I’ve learned about doing Nginx redirects correctly, including the mistakes that taught me the hard way.
Understanding return vs rewrite
Nginx gives you two main tools for redirects, and knowing when to use each one matters:
return— the simpler, faster option. Use this for straightforward redirects where you’re not doing complex pattern matching.rewrite— more powerful, supports regular expressions and can rewrite the internal request URI (not just redirect the client). Use this whenreturngenuinely can’t express what you need.
My rule of thumb, which matches Nginx’s own documentation advice: prefer return whenever possible. It’s evaluated earlier in Nginx’s request processing and is more predictable. Reach for rewrite only when you need regex capture groups or more complex logic.
301 vs 302: Which One Do You Actually Want?
This distinction trips up more people than anything else in this guide, so let’s be precise:
- 301 Moved Permanently — tells browsers and search engines this redirect is permanent. Browsers cache it aggressively, and search engines will transfer SEO ranking signals to the new URL. Use this for permanent moves: domain changes, permanent URL restructuring, HTTP→HTTPS redirects.
- 302 Found (temporary redirect) — tells clients this is temporary and they should keep checking the original URL in the future. Search engines won’t transfer ranking. Use this for genuinely temporary situations: maintenance pages, A/B tests, temporary promotional redirects.
I’ve seen real SEO damage from using 302s for what were actually permanent moves — search engines kept indexing the old URL for months because they were told, explicitly, not to treat the move as permanent. Get this right; it matters more than it seems like it should.
Basic Redirect Syntax
The simplest form, redirecting one specific URL to another:
server {
listen 80;
server_name example.com;
location = /old-page {
return 301 /new-page;
}
}
The = in location = /old-page means an exact match — this only fires for that precise path, not for /old-page/anything-else.
Redirecting an Entire Domain (or Subdomain) to Another
This is one of the most common real-world cases — migrating from oldsite.com to newsite.com, or from a non-www to www version of your domain (or vice versa).
server {
listen 80;
listen 443 ssl;
server_name oldsite.com www.oldsite.com;
ssl_certificate /etc/letsencrypt/live/oldsite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/oldsite.com/privkey.pem;
return 301 https://newsite.com$request_uri;
}
$request_uri is important here — it preserves the original path and query string, so oldsite.com/blog/my-post?ref=twitter correctly redirects to newsite.com/blog/my-post?ref=twitter instead of dropping everything after the domain.
Redirecting HTTP to HTTPS
This is a redirect nearly every production site needs, and it’s simple:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# your actual site config here
}
I use $host here instead of hardcoding the domain name, so this same block works correctly for both example.com and www.example.com without needing to duplicate it. If you’re using Certbot, it typically sets this up for you automatically when you run certbot --nginx.
Redirecting www to non-www (or the Reverse)
Pick one canonical version of your domain and stick to it — having both example.com and www.example.com serving identical content without a redirect between them is a duplicate-content problem for SEO.
Redirecting www to non-www:
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
return 301 https://example.com$request_uri;
}
Redirecting non-www to www is just the mirror image — swap which server_name gets the redirect and which one is canonical.
Using rewrite for Pattern-Based Redirects
Sometimes you need to redirect based on a pattern rather than an exact path. This is where rewrite earns its place. Say you’re migrating from /blog/2023/my-post to /articles/my-post — dropping the year segment across every old blog post:
location /blog/ {
rewrite ^/blog/[0-9]{4}/(.*)$ /articles/$1 permanent;
}
permanent here is shorthand for a 301 redirect (there’s also redirect, which issues a 302). The (.*)$ captures everything after the year, and $1 in the destination reuses that captured value.
I’ll be honest: I reach for return with map blocks for anything beyond the simplest regex pattern, because deeply nested rewrite rules get hard to reason about and even harder to debug six months later. For example, redirecting a specific list of old paths to new ones using a map:
map $uri $new_uri {
/old-about /about;
/old-contact /contact;
/old-pricing /pricing;
}
server {
listen 80;
server_name example.com;
if ($new_uri) {
return 301 $new_uri;
}
}
This scales much better than a pile of individual location blocks when you have dozens of one-off redirects to manage, like after a site restructuring.
Redirecting a Whole Path Prefix
If you moved an entire section of your site — say /shop/ became /store/ — and want every URL underneath it preserved:
location /shop/ {
rewrite ^/shop/(.*)$ /store/$1 permanent;
}
This correctly turns /shop/shoes/running into /store/shoes/running, preserving everything after the prefix.
Avoiding Redirect Loops
This is the mistake I mentioned at the start, and it’s worth dwelling on because it’s so easy to create by accident. A redirect loop happens when a redirect rule’s destination also matches the same rule (directly, or indirectly through another redirect that points back).
A classic way to accidentally create one: combining an HTTP→HTTPS redirect with a www→non-www redirect in the wrong order, or with conflicting server_name blocks, so the browser gets bounced between the two rules indefinitely.
To avoid this, I always:
- Test redirect chains manually with
curl -IL(the-Lfollows redirects,-Ishows headers only), so I can see the entire chain of hops before deploying - Keep redirect logic in as few places as possible — ideally, exactly one canonical redirect chain: HTTP → HTTPS → canonical domain, in that order, never circular
- Never redirect a URL to itself, even conditionally — double check any
maporrewriteoutput against its own input
Testing a redirect chain:
curl -IL http://www.oldsite.com/some-page
This shows every hop, in order, with status codes — if you see the same URL appear twice, you’ve got a loop.
Testing Your Redirects
sudo nginx -tbefore every reload, alwayscurl -I https://example.com/old-page— confirm theLocationheader points where you expect and the status code is what you intended (301 vs 302)curl -IL https://example.com/old-page— follow the full chain and make sure it terminates in a200, not a loop- Test with query strings attached, to confirm
$request_uri(or$is_args$argsif you’re constructing the URL manually) is preserving them correctly - Test in an actual browser too — some redirect issues (like mixed content warnings after HTTP→HTTPS redirects) only show up there
Troubleshooting Common Issues
“Too many redirects” in the browser — You have a loop. Trace it with curl -IL and look for a repeating pattern in the hops.
Query strings getting dropped — You likely used a hardcoded destination instead of appending $request_uri or $is_args$args.
Redirect not firing at all — Check location block specificity; a more specific block elsewhere in your config might be matching first. Nginx’s location matching order (exact match, then longest prefix match, then regex in the order they appear) is worth reviewing if a redirect seems to be silently ignored.
Search engines still showing old URLs months later — Confirm you used 301, not 302. Also confirm the redirect has actually been live and consistent — search engines need to consistently see the 301 across multiple recrawls before fully updating their index.
Mixed content warnings after HTTP→HTTPS redirect — This isn’t an Nginx issue; it means your HTML is hardcoding http:// URLs for internal assets. Fix these in your application code to use protocol-relative or HTTPS URLs.
Security Considerations
- Don’t build redirect destinations directly from unsanitized user input (like an unvalidated
Refererheader) — this can be abused for open-redirect vulnerabilities, where an attacker crafts a link through your trusted domain that redirects to a malicious site - If you accept a redirect target as a URL parameter (e.g.,
?returnUrl=), validate it against an allowlist of known-safe paths before using it in areturndirective - Use 301s deliberately and permanently — don’t use them for anything you might need to reverse later, since browsers cache them aggressively and users may not see the old URL work again for a long time even after you remove the redirect
Performance Tips
- Prefer
returnoverrewritewhere possible — it’s evaluated earlier in Nginx’s processing pipeline and avoids the overhead of full regex evaluation - Consolidate many individual redirects into a
mapblock rather than dozens of separatelocationblocks — it’s both easier to maintain and slightly more efficient - Avoid redirect chains longer than one hop wherever you can — each hop is a full round trip for the client, adding latency; if you find yourself with A→B→C, just redirect A→C directly
Real-World Use Case
During a domain rebrand, I had to redirect an entire old domain, preserve full paths and query strings, force HTTPS, and canonicalize away from www — all without breaking existing inbound links from years of accumulated backlinks and bookmarks. The final setup was: one server block on the old domain catching both HTTP and HTTPS, issuing a single 301 straight to the new canonical HTTPS non-www URL with $request_uri appended, avoiding any intermediate hops. Testing with curl -IL before going live caught an early version that had an accidental loop between the www and non-www rules — a five-minute test that saved what would have been a very bad launch day.
Best Practices Recap
- Use
returnoverrewriteunless you need regex capture groups - 301 for permanent moves, 302 for genuinely temporary ones — this distinction has real SEO consequences
- Always preserve
$request_uri(or query strings) unless you have a specific reason not to - Test every redirect chain with
curl -ILbefore considering it done - Consolidate many one-off redirects into a
mapblock rather than sprawlinglocationblocks - Never redirect based on unsanitized user input
Redirects are one of those Nginx features that are simple in isolation and genuinely dangerous in combination — each individual rule usually makes sense on its own, but chains of them interacting is where loops and dropped query strings sneak in. Testing the full chain with curl -IL, every single time, is the one habit that’s saved me the most grief.
