Nginx rewrite vs return 301: tested redirect examples

Updated 4 November 2026 · By Brent Alexis

Short answer: for a normal redirect in Nginx, use return 301 with $request_uri, for example return 301 https://example.com$request_uri;. It doesn't run a regex, it can't match the wrong URLs by accident, and it keeps the path and query string. Use rewrite only when you need to capture part of the old URL and reuse it, like moving a whole folder.

Every example and trap on this page was checked with real requests on Nginx 1.27. How it's tested

return vs rewrite

returnrewrite
What it doesStops and sends the response straight awayMatches the URL against a regex and changes it
Redirect codesAny code you write: 301, 302, 307, 308permanent = 301, redirect = 302
Query stringOnly what you include ($request_uri or $is_args$args)Added automatically; a trailing ? drops it
With no flagn/aRelative target: rewrites inside Nginx, no redirect. Full URL: a 302
Best forHTTP to HTTPS, www, domain moves, single pagesMoved folders and other URL patterns

If you searched for an Nginx rewrite generator: for the everyday jobs below you want return 301, and the Redirect Generator writes it for you.

Tested redirect examples

Change example.com to your domain. The examples that listen on port 80 keep Let's Encrypt renewals working: the check file is served from /var/www/html (the folder you passed to certbot -w), and anything else falls back to the redirect.

HTTP to HTTPS

server {
    listen 80;
    server_name example.com www.example.com;

    # Let's Encrypt renewals (certbot --webroot): serve the check file from the folder you
    # passed to certbot -w. If it isn't there, the request falls back to the redirect below.
    location /.well-known/acme-challenge/ {
        root /var/www/html;
        try_files $uri @redirect;
    }
    location @redirect {
        return 301 https://$host$request_uri;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

Checked: /a/b?x=1 goes to https://example.com/a/b?x=1, www keeps its own host, the renewal check file is still served, and a missing check file falls back to the redirect. This sits next to your existing HTTPS server block.

Bare domain to www

server {
    listen 80;
    listen 443 ssl;
    server_name example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
        try_files $uri @redirect;
    }
    location @redirect {
        return 301 https://www.example.com$request_uri;
    }

    location / {
        return 301 https://www.example.com$request_uri;
    }
}

Checked over HTTP and HTTPS. It answers HTTPS on the bare domain too, so the bare domain needs its own certificate.

www to bare domain

server {
    listen 80;
    listen 443 ssl;
    server_name www.example.com;
    ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
        try_files $uri @redirect;
    }
    location @redirect {
        return 301 https://example.com$request_uri;
    }

    location / {
        return 301 https://example.com$request_uri;
    }
}

Checked over HTTP and HTTPS. The www name needs a certificate for the same reason.

Old domain to new domain

server {
    listen 80;
    server_name old.example www.old.example;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
        try_files $uri @redirect;
    }
    location @redirect {
        return 301 https://new.example$request_uri;
    }

    location / {
        return 301 https://new.example$request_uri;
    }
}

Checked: old.example/a?x=1 goes to https://new.example/a?x=1, with or without www. Keep the old domain registered and pointing at this server for as long as people still use the old links.

One page

location = /old-page {
    return 301 https://example.com/new-page;
}

This drops any query string: /old-page?x=1 goes to /new-page. To keep it, add $is_args$args, which adds ?x=1 only when there is one:

location = /old-page {
    return 301 https://example.com/new-page$is_args$args;
}

A moved folder (the job for rewrite)

rewrite ^/blog/(.*)$ /articles/$1 permanent;

Checked: /blog/post-1?x=1 goes to /articles/post-1?x=1. Put it inside the server block. (.*) captures the rest of the path and $1 puts it back.

Five rewrite traps

Each of these passes nginx -t and does something you might not expect:

  1. A full URL with no flag is a 302, not a 301. rewrite ^/old$ https://example.com/new; redirects, but as a temporary move, which tells search engines to keep the old URL. Add permanent, or use return 301.
  2. No $ means "starts with", and server-level rewrites run first. rewrite ^/old /new permanent; also sends /oldest and /old-stuff to /new. Worse, a rewrite in the server block runs before Nginx even picks a location, so it also swallowed /old-page in the lab, even though a location = /old-page block was there to handle it. Anchor both ends (^/old$), or use location = /old with return.
  3. rewrite keeps the query string unless you stop it. rewrite ^/q$ /new permanent; turns /q?a=1 into /new?a=1. End the target with ? (/new?) to drop it.
  4. A relative target with no flag isn't a redirect at all. rewrite ^/inside$ /target.txt; serves target.txt under the old URL, and the browser never sees a change. That's handy for pretty URLs, but wrong for moving a page: visitors and search engines keep using the old address.
  5. $uri isn't $request_uri. return 301 https://$host$uri; loses the query string. Use $request_uri in redirects.

301, 302, 307 or 308?

301 Moved Permanently
The page has moved for good. Search engines move its ranking to the new URL, and browsers cache it hard, so test with a 302 or in a private window first.
302 Found
Temporary: a sale page, maintenance, a short campaign. Search engines usually keep showing the old URL.
307 and 308
The same as 302 and 301, except the browser must keep the request method, so a form POST stays a POST. With 301 and 302, browsers may switch it to GET. Use 308 when moving API endpoints or form handlers: return 308 https://example.com/api;

Test before you reload

sudo nginx -t && sudo systemctl reload nginx
curl -I "http://example.com/some/page?x=1"

Check the status line and the Location: header. Use curl or a private window rather than your normal browser, which may have cached an earlier 301. If a reload seems to change nothing, see reload said OK, but my change didn't apply.

On Apache? The Redirect Generator writes the same redirects as .htaccess rules or a virtual host.

Generate your Nginx redirect →

How to use ProxyForge · FAQ: common errors · Guides · Apache: .htaccess vs virtual host · What's new · About