The cleanest way to add a permanent redirect in Nginx is a dedicated server block that uses the return directive — for example, to force HTTPS:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
That single line sends a 301 Moved Permanently and preserves the original path and query string via $request_uri. Prefer return over rewrite ... permanent for fixed redirects: it is faster, clearer, and avoids the regex engine entirely. Reach for rewrite only when you genuinely need pattern matching or capture groups.
This guide covers every common redirect you will actually need in 2026 — HTTP→HTTPS, www canonicalization, trailing slashes, old-path→new-path, full-domain moves, and bulk one-off redirects with the map directive — plus the SEO rules that keep your rankings intact.
Key takeaways
- Use
return 301 https://example.com$request_uri;in a dedicatedserverblock for permanent redirects — it is cheaper and clearer thanrewrite. - 301 is permanent and cached hard by browsers and search engines. It passes ranking signals, but it is sticky and difficult to undo — always test before you ship it.
- 302 / 307 are temporary; 308 is the permanent twin of 307 that preserves the HTTP method (use it when you must keep a
POST). - Preserve the path with
$request_uriand avoid redirect chains and loops — each hop adds latency and dilutes link equity. ifis evil inlocationcontext — use separateserverblocks or themapdirective instead of stuffing redirect logic intoif.- Verify everything with
curl -Ibefore and after deploying, and update your internal links so they point straight at the final URL.
301 vs 302 vs 307 vs 308: which status code?
The redirect status code tells browsers and search engines whether the move is permanent and whether the request method must be preserved. Picking the wrong one is the most common — and most expensive — redirect mistake for SEO.
| Status | Meaning | Permanent? | Method preserved? | Cached by browser | SEO / ranking signals |
|---|---|---|---|---|---|
| 301 | Moved Permanently | Yes | Not guaranteed (clients may switch POST→GET) | Yes, aggressively | Passes ranking signals; consolidates to the new URL |
| 302 | Found (temporary) | No | Not guaranteed | No | Original URL keeps ranking; new URL not indexed as canonical |
| 307 | Temporary Redirect | No | Yes | No | Temporary; original URL retains signals |
| 308 | Permanent Redirect | Yes | Yes | Yes | Passes ranking signals like a 301, but keeps the method |
Rules of thumb:
- Moving a page or domain for good, and it is a normal
GETpage? Use 301. It is the SEO workhorse. - Short-lived redirect (A/B test, maintenance page, geo-routing you will reverse)? Use 302 or 307.
- Need to preserve a
POSTbody across the redirect (API or form endpoint)? Use 307 (temporary) or 308 (permanent). A 301/302 may silently downgradePOSTtoGET.
Where redirect config lives
On most Debian/Ubuntu systems your site config lives in /etc/nginx/sites-available/ and is symlinked into /etc/nginx/sites-enabled/. On RHEL/Alma/Rocky and many container images it lives under /etc/nginx/conf.d/*.conf. Edit the file for your site, then validate and reload (never blindly restart) so a typo can't take the server down:
# Test the configuration for syntax errors first
sudo nginx -t
# If the test passes, reload without dropping live connections
sudo systemctl reload nginx
# (older systems: sudo service nginx reload)Recipe 1: Redirect all HTTP to HTTPS
The single most important redirect in 2026. Keep one minimal server block on port 80 whose only job is to bounce every plaintext request to HTTPS, and serve your real site from the 443 block. If you haven't set up TLS yet, see our guide to configuring free SSL with Let's Encrypt and Nginx.
# Port 80: redirect everything to HTTPS, then stop.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
# Port 443: the real site (canonical, non-www host).
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# ... your root, location blocks, etc.
}Note the modern http2 on; directive — the older listen 443 ssl http2; form is deprecated in current Nginx. For more on why HTTP/2 is worth enabling, see how HTTP/2 makes the web faster and safer.
Recipe 2: Canonicalize www ↔ non-www
Pick one canonical host and 301 the other to it so search engines never split ranking signals across two hostnames. Do this in a dedicated server block — not with an if inside your main block.
Redirect www → non-www (the example below assumes a separate non-www block serves the real site):
# Drop the www. -> canonical bare domain (permanent).
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
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;
}To go the other direction (non-www → www), simply swap the hostnames — server_name example.com; and return 301 https://www.example.com$request_uri;. The pattern is identical; only the canonical host changes.
Recipe 3: Redirect a single old page to a new one
When you rename or consolidate a page, send the old URL to its replacement with an exact-match location and a return 301. Exact match (location = /old-page) is the most efficient — Nginx stops searching as soon as it matches.
# Exact one-to-one page move.
location = /old-page.html {
return 301 /new-page/;
}
# Move an entire old section to a new prefix, preserving the sub-path.
# /docs/v1/intro -> /guides/intro
location /docs/v1/ {
rewrite ^/docs/v1/(.*)$ /guides/$1 permanent;
}The first block uses return for a fixed target. The second needs a capture group to carry the rest of the path across, so rewrite ... permanent (which emits a 301) is the right tool. The permanent flag = 301; use redirect for a 302.
Recipe 4: Move an entire domain
Migrating old-domain.com to new-domain.com? Keep the old domain's certificate valid, point it at Nginx, and 301 every path to the new domain. This preserves deep links and passes the maximum ranking signal to the new home.
# Whole-domain move: every old path lands on the same path on the new domain.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name old-domain.com www.old-domain.com;
ssl_certificate /etc/letsencrypt/live/old-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/old-domain.com/privkey.pem;
return 301 https://new-domain.com$request_uri;
}Recipe 5: Many one-off redirects with the map directive
When you have dozens of unrelated old→new URLs (a typical CMS migration), don't write dozens of location blocks or — worse — a wall of if statements. Define a map in the http context and look the path up once per request. It's fast (hash lookup) and trivial to maintain.
# In the http { } context (e.g. /etc/nginx/conf.d/redirects.conf):
map $request_uri $redirect_target {
default "";
/old-pricing /plans/;
/blog/old-slug /blog/new-slug/;
/careers.html /jobs/;
/products/legacy-widget /products/widget/;
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
# ... ssl + root ...
# If this path has a mapped target, 301 to it; otherwise fall through.
if ($redirect_target) {
return 301 $redirect_target;
}
}This is the one place an if is safe: it sits in server context (not location) and does nothing but return, which is on Nginx's short list of directives that are safe inside if. The official wiki's "If Is Evil" warning is specifically about if inside a location block, where it can produce surprising, broken behaviour. Avoid it there.
Recipe 6: Trailing slashes and query strings
Keep one canonical form for trailing slashes so you don't serve duplicate URLs. To add a trailing slash to a known directory path:
# Force a trailing slash on a specific path.
location = /guides {
return 301 /guides/;
}
# Redirect but DROP the query string: end the target with '?'.
location = /search.php {
return 301 /search/?; # trailing ? discards the incoming ?q=...
}
# Redirect and KEEP the query string (the default with $request_uri).
location = /find {
return 301 /search/$is_args$args;
}By default Nginx appends the original query string to a redirect target that contains variables; ending the target with a bare ? strips it. Use $is_args$args (or $request_uri, which already includes the query) when you want to carry parameters through.
SEO best practices for Nginx redirects
- Use 301 for permanent moves. It consolidates ranking signals onto the new URL. A 302 leaves the old URL as the canonical one in Google's eyes.
- 301s are cached hard by browsers and can persist for a long time. Roll out a temporary change as a 302/307 first; switch to 301 only once you're certain.
- Preserve the exact path with
$request_uriwhenever possible. A blanket redirect of every old URL to the homepage is treated as a soft 404 and loses the page's equity. - Avoid redirect chains and loops.
http → https → www → non-wwwis three hops; collapse them so the first response lands on the final canonical URL. - Update internal links, sitemaps, and canonicals to point at the destination directly. Redirects are a safety net, not a substitute for clean links.
- Don't mix a 301 with a conflicting
rel=canonicalthat points elsewhere — pick one signal and be consistent.
Test every redirect with curl
Never trust a redirect you haven't inspected. curl -I (or curl -sI) shows the status line and Location header without downloading the body, and -L follows the full chain so you can count the hops:
# Inspect a single redirect: status code + Location header.
curl -I http://example.com
# HTTP/1.1 301 Moved Permanently
# Location: https://example.com/
# Follow the whole chain and print each hop's status + URL.
curl -sIL -o /dev/null -w '%{http_code} %{url_effective}\n' http://www.example.com/old-page
# Verify a POST is preserved across a 307/308 (method should stay POST).
curl -ILX POST https://example.com/api/legacyA healthy permanent redirect returns exactly one 301 and a single Location pointing at the final HTTPS canonical URL. If you see a 302 where you expected a 301, or two-plus hops, fix the config before search engines crawl it.
Redirects are only one slice of a hardened Nginx setup. For related server tasks, see HTTP basic-auth with Nginx and running Django behind Nginx with uWSGI. If you'd rather hand this off entirely, our team offers server maintenance and DevOps services covering Nginx, TLS, and zero-downtime migrations.
Frequently Asked Questions
Should I use return or rewrite for redirects in Nginx?
Prefer return for fixed, one-to-one redirects: return 301 https://example.com$request_uri; is faster and clearer because it skips the regex engine entirely. Use rewrite ... permanent; only when you need pattern matching or capture groups to carry part of the path into the destination.
What is the difference between a 301 and a 302 redirect?
A 301 is permanent: it tells browsers and search engines the resource has moved for good, passes ranking signals to the new URL, and is cached aggressively. A 302 is temporary: the original URL stays canonical, signals are not transferred, and browsers do not cache the mapping. Use 301 for permanent moves and 302 for short-lived ones.
When should I use a 307 or 308 redirect instead?
Use 307 (temporary) or 308 (permanent) when the request method must be preserved — most importantly to keep a POST as a POST. A 301 or 302 may legally downgrade a POST to a GET, which breaks form and API endpoints. 308 behaves like a 301 for SEO but guarantees the method is kept.
How do I redirect HTTP to HTTPS in Nginx?
Keep a minimal server block listening on port 80 whose only directive is return 301 https://example.com$request_uri;, and serve your real site from the 443 block. The $request_uri variable preserves the original path and query string so visitors land on the exact HTTPS equivalent of the page they requested.
Why is using if for redirects discouraged in Nginx?
The "If Is Evil" rule applies to if inside a location block, where it can interact unpredictably with other directives and silently break responses. Redirect logic belongs in dedicated server blocks, exact-match location blocks, or a map directive. The one safe exception is an if in server context that does nothing but return.
How do I test that a redirect is working correctly?
Use curl -I https://example.com/old-url to see the status line and Location header without downloading the page. Add -L to follow the full chain and confirm there is only one hop ending on your canonical URL. A correct permanent redirect returns a single 301 and a Location header pointing at the final HTTPS address.