Skip to content

How to Use URL Rewriting?

Updated 5 min read

Updated: 22 October 2026 · Applies to: Nginx on a self-managed Linux VPS; commands tested against nginx 1.31 (Alpine)

Short answer: use return when you want the browser to see a redirect (a new HTTP status and a new URL in the address bar). Use rewrite when you want Nginx to serve different content for a URL without the browser ever knowing the URL changed. Both directives come from Nginx's core ngx_http_rewrite_module, so no extra module needs installing.

What you need

  • Root or sudo access to a server running Nginx, and the ability to edit its config and reload the service.
  • A way to test a rule before it goes live. The examples below were tested in a throwaway nginx:alpine container; you can repeat the same test with Docker or Podman on any machine, or in a staging VPS, before touching a production config.
  • A basic regular-expression tester such as regex101.com helps when writing the capture-group patterns used below.

Return: send the browser a redirect

return is the simpler of the two directives. It does not use regular expressions on its own, but it can reuse a group captured by the location block it sits in. Place it inside a server or location block.

Redirect everything under a path to another domain, permanently:

location /old-blog/ {
    return 301 https://example.com;
}

Keep the original path and query string when redirecting to the new domain, using $request_uri:

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

A request to /old-blog/post1?x=1 becomes https://example.com/old-blog/post1?x=1. Use $host instead of a hard-coded domain if you want the rule to work the same way on every domain the server answers for.

Redirect a request based on a pattern captured in the location block, for example sending any .txt file to a /files/ directory:

location ~* ^/([^/]+\.txt)$ {
    return 301 /files/$1;
}

A request for /report.txt redirects to /files/report.txt. (Escape the dot as \. in the pattern, an unescaped dot matches any character, not just a literal one.)

Rewrite: change the URL internally

Syntax: rewrite regex replacement [flag]; The regex is matched against the request URI; when it matches, the URI is replaced and, depending on the flag, Nginx either keeps processing in the current location or looks for a new location that matches the new URI. The flags, per the official module documentation:

  • last: stop processing rewrite rules in this location and start a fresh location search on the new URI.
  • break: stop processing rewrite rules, but stay in the current location; the rewritten URI is served from there, it is not matched against other locations.
  • redirect: send the browser a temporary 302 redirect (used automatically when the replacement is not a local path).
  • permanent: send the browser a permanent 301 redirect.

Official documentation: nginx.org, ngx_http_rewrite_module.

last vs break, tested side by side

The difference between last and break only shows up once a rewritten URL would also match a different location block. Copy this into a test container to see it for yourself:

location /browser/ {
    rewrite ^/browser/(.*)$ /chrome/$1 last;
}
location /chrome/ {
    try_files $uri $uri/ =404;
}

location /break-test/ {
    rewrite ^/break-test/(.*)$ /served/$1 break;
    try_files $uri $uri/ =404;
}
location /served/ {
    return 200 "this text is never reached by the break example below\n";
}

With that config loaded and a file called sample.txt placed in both the chrome and served directories under the document root:

nginx -t && nginx -s reload
curl http://127.0.0.1/browser/sample.txt
curl http://127.0.0.1/break-test/sample.txt

Both requests return the content of sample.txt, which looks identical from the outside, but the two flags got there differently. The last request was rewritten to /chrome/sample.txt and then re-matched against the /chrome/ location, which served it with try_files. The break request was rewritten to /served/sample.txt but never re-entered the /served/ location at all. try_files, still running inside the original /break-test/ location, served the file straight from the document root. That is why the return 200 text inside /served/ never appears: with break, that location block is never reached, no matter what it contains.

Test and apply a rule safely

  1. Edit the config in a server or location block, in a separate file first if possible.
  2. Check the syntax before touching the live service:
    nginx -t
  3. Reload once the test passes, so existing connections are not dropped:
    systemctl reload nginx
    (or service nginx reload on older init systems; on a cPanel/WHM server running EA-Nginx, reload it from WHM's Service Manager so the panel's own state stays in sync).
  4. Confirm the result with a direct request, following redirects where relevant:
    curl -I http://your-domain.example/old-blog/post1
    curl -IL http://your-domain.example/old-blog/post1

Common mistakes to avoid

  • Redirecting a domain to itself with return or rewrite, which creates a redirect loop the browser eventually refuses to follow.
  • Using last inside a location block when the rewritten URI matches the same location again. Nginx stops after 10 internal rewrites and returns a 500 error rather than loop forever.
  • Leaving an unescaped dot in a file-extension pattern, for example .txt instead of \.txt, which matches one character followed by "txt" rather than a literal dot.
  • Forgetting that a query string on the original request is appended again to a rewrite replacement that already has its own arguments, unless the replacement ends with a bare ?.

Related services

Managed cPanel VPS

cPanel included; we handle updates, security and monitoring.

cPanel server management

hardening, migrations and 24/7 monitoring, with no long-term contract.

cPanel, Plesk and LiteSpeed licences

for VPS and dedicated servers, with installation help on demand.

Was this answer helpful?

Still stuck? Open a ticket