Create Nginx Rewrite Rules with return and rewrite

Create Nginx rewrite rules with return and rewrite. Covers flags, query-string preservation, duplicate query strings, curl tests, and reloads.

Last updatedAuthorJoshua JamesRead time11 minGuide typeNginx

Nginx rewrite rules are useful when a URL move needs more than a one-off redirect, but they can also cause loops, duplicate query strings, or open redirects when the pattern is too broad. Use return for direct redirects and status responses; reserve rewrite for URI transformations that need regex captures.

Before pasting a rule, identify the server or location block that owns the request, decide whether query strings should stay or be dropped, then run nginx -t, reload, and test the response before production traffic depends on the change.

Check Nginx Context Before Writing Rewrite Rules

Before editing rewrite rules, make sure Nginx is installed and running on the server that will receive the change. If you still need the base package, use the matching installation guide for installing Nginx on Ubuntu, installing Nginx on Debian, or installing Nginx on Fedora.

On systemd-based Linux servers, verify Nginx is active before proceeding:

sudo systemctl is-active nginx

A running service returns:

active

Nginx configuration files are typically located in /etc/nginx/. Server blocks, also called virtual hosts, often live in /etc/nginx/sites-available/ on Debian-based systems and /etc/nginx/conf.d/ on RHEL-based systems. The rewrite syntax itself is the same across platforms, but the file you edit depends on how your Nginx package or build organizes server blocks.

Do not use rewrite as the first answer for every path change. If the request is being proxied and the goal is to strip or replace a prefix such as /api/, Nginx proxy_pass trailing slash behavior may own that URI change. Use the broader guide to create a reverse proxy in Nginx when the backend, headers, and upstream service still need setup.

Choose Between Nginx return and rewrite

The first choice is whether the rule needs return or rewrite. They can overlap, but they do not behave the same way. Choose the simplest directive that expresses the redirect or URI change clearly.

Aspectreturn Directiverewrite Directive
Primary useSimple redirects, status codes, short responsesRegex-based URI transformations
Regex supportThe directive itself has no regex matcherUses PCRE regular expressions
Capture groupsCan use variables, but does not create $1 capturesCreates $1, $2, and later captures from the regex
Redirect behaviorStops processing and returns the selected responseCan redirect externally or rewrite internally depending on replacement and flag
Best forDomain redirects, HTTPS redirects, canonical host redirects, 403/410/503 responsesLegacy path restructuring, extensionless routes, clean URLs, capture-based redirects

Use return when the destination is known without parsing the URI. Use rewrite when the rule must capture part of the request path or convert one route shape into another. For more details on conditional logic, see the guide to Nginx if-else directives.

Redirect URLs with the Nginx return Directive

The return directive stops request processing immediately and sends a response to the client. It works in server, location, and rewrite-module if contexts. Because return does not evaluate its own regex, it is the cleanest choice for simple redirects and status responses.

The basic syntax is:

return code [text|URL];

For redirects, use status codes 301 (permanent) or 302 (temporary). A 301 redirect tells browsers and search engines that the URL has permanently moved, so they should update their records. A 302 redirect indicates a temporary move.

Redirect an Entire Domain

When migrating to a new domain, you can redirect all requests while preserving the original path. The $request_uri variable contains the path and query string from the original request:

server {
    listen 80;
    server_name oldsite.com www.oldsite.com;
    return 301 https://newsite.com$request_uri;
}

With this configuration, a request to http://oldsite.com/products/widget?color=blue redirects to https://newsite.com/products/widget?color=blue. The $request_uri variable contains the original path and query string, so it preserves both pieces during the redirect.

Enforce HTTPS Connections

Redirecting HTTP traffic to HTTPS is one of the most common uses for the return directive. This configuration listens on port 80 and redirects all requests to the secure version:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

The $host variable preserves the original hostname, so requests to both example.com and www.example.com redirect to their HTTPS equivalents. For related configuration options, see our guide on redirecting www and non-www URLs in Nginx.

Return Custom Status Codes

The return directive can also send non-redirect status codes. For maintenance pages, access denial, or API responses, you can return codes like 403 (Forbidden), 404 (Not Found), or 503 (Service Unavailable):

server {
    listen 80;
    server_name example.com;

    location /admin {
        return 403;
    }

    location /api/v1 {
        return 410 "This API version has been deprecated. Please use /api/v2";
    }
}

The 410 (Gone) status code indicates that the resource existed previously but has been permanently removed, which helps search engines clean up their indexes.

Transform URLs with the Nginx rewrite Directive

The rewrite directive modifies the request URI using PCRE regular expressions. It works in server, location, and rewrite-module if contexts. Unlike return, rewrite can capture parts of the URI and reuse them in the replacement string.

The syntax is:

rewrite regex replacement [flag];

The regex pattern matches the normalized URI without the query string. Captured groups are available as $1, $2, and later captures in the replacement string. For local rewrites, make the replacement resolve to a URI that starts with /; a capture such as $1 is valid only when the captured text includes that leading slash.

Understand Nginx rewrite Flags

The flag parameter determines what Nginx does after the replacement. The official Nginx rewrite module documentation lists four flags, and omitting the flag is its own behavior:

FlagResultUse Case
No flagFor a local replacement, changes the URI and continues processing the remaining rewrite-module directives in the current context. For an absolute replacement, returns a 302 redirect. It is not the same as last.Rare cases where another local rewrite directive should still run, or a temporary absolute redirect is intentional. Use an explicit flag when possible.
lastStops the current rewrite set and starts a new location search for the changed URI.Server-level rewrites or rewrites that intentionally move the request into another location.
breakStops the current rewrite set and keeps processing inside the current location.Location-level rewrites where a new location search could match the same block again.
redirectReturns a 302 temporary redirect when the replacement is a local URI.Temporary redirects that users and crawlers should not treat as permanent.
permanentReturns a 301 permanent redirect.Stable URL moves that browsers and search engines can cache as permanent.

If the replacement string starts with http://, https://, or $scheme, Nginx stops rewrite processing and returns a redirect to the client. Without a flag, that absolute redirect is temporary. Add permanent when the redirect should be 301; use return instead when the destination is already known and no regex captures are needed.

Control Query Strings in Nginx rewrite Rules

Nginx rewrite keeps the original query string by default when the replacement does not define a new one. Add a trailing ? to the replacement when the old query string should not be appended.

# Keeps the original query string
rewrite ^/old$ /new permanent;
# /old?debug=1 redirects to /new?debug=1

# Drops the original query string
rewrite ^/old$ /new? permanent;
# /old?debug=1 redirects to /new

The return directive does not use this rewrite argument-appending rule. Preserve the original path and query string in a return redirect by including $request_uri. If you build a different destination manually, use $is_args$args only when you intentionally want to append the current query string.

# Drops the original query string
return 301 /new;

# Preserves the original query string on a different path
return 301 /new$is_args$args;

Use the preserving form only when the new path accepts the same parameters. Dropping stale filters, tracking parameters, or application-specific arguments is often safer during permanent URL cleanup.

Avoid Duplicate Query Strings with $request_uri

The $request_uri variable already contains the original path and query string. In a rewrite redirect, adding $request_uri without a trailing ? can duplicate the query string because rewrite appends the old request arguments by default.

# Prefer return when no regex capture is needed
return 301 https://example.com$request_uri;

# If rewrite must use $request_uri, add ? to stop argument appending
rewrite ^ https://example.com$request_uri? permanent;

A request for /old?debug=1 becomes https://example.com/old?debug=1 with either corrected pattern. Without the trailing ? on the rewrite replacement, the same request can become https://example.com/old?debug=1?debug=1.

Choose Between Nginx last and break

The difference between last and break causes confusion, but the rule is straightforward: use last in server blocks and break inside location blocks that might match the rewritten URL.

Consider this example from the official Nginx documentation:

server {
    listen 80;
    server_name example.com;
    
    # Using 'last' in server context - triggers new location search
    rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 last;
    rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra last;
    return 403;
}

However, if these rules are moved inside a location block, you must use break to prevent infinite loops:

location /download/ {
    # Using 'break' prevents re-matching this location
    rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 break;
    rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra break;
}

With last inside the location block, Nginx would rewrite the URL, search for a matching location, match /download/ again, and repeat up to 10 times before returning a 500 error.

Practical Nginx Rewrite Rule Examples

Start with common URL transformation scenarios, then adjust only the server name, source path, destination path, and query-string decision that match your site.

Build a Rewrite Rule from a Checklist

Generic rewrite generators cannot know your Nginx context, location priority, upstream routing, or query-string policy. Use this checklist before turning a pattern into a live rule:

  • Choose return for direct redirects and rewrite only when regex captures or internal URI changes are required.
  • Place broad redirects in a server block and path-specific rules in the narrowest safe location.
  • Decide whether the destination keeps, replaces, or drops the original query string before writing the replacement.
  • Use last only when the rewritten URI needs a new location search; use break inside locations that could match themselves again.
  • Run sudo nginx -t, reload Nginx, and verify the exact status code and Location header with curl.

Redirect a Directory to a New Location

When reorganizing your site structure, redirect an entire directory while preserving the path hierarchy:

server {
    listen 80;
    server_name example.com;
    
    location ^~ /old-directory/ {
        rewrite ^/old-directory/(.*)$ /new-directory/$1 permanent;
    }
}

The ^~ modifier gives this block priority over regex locations; the separate Nginx location block priority reference covers the full matching order. The rewrite captures everything after /old-directory/ in the $1 group and appends it to the new path. A request to /old-directory/products/item.html redirects to /new-directory/products/item.html.

Route Clean URLs to Query-Based Scripts

Many older applications still expect query parameters even when users visit cleaner path-based URLs. This internal rewrite lets a request such as /products/123/page/2 reach the application as /product.php?id=123&page=2 without changing the browser address.

server {
    listen 80;
    server_name example.com;
    
    location /products {
        rewrite ^/products/([0-9]+)/page/([0-9]+)$ /product.php?id=$1&page=$2 last;
    }
}

With this configuration, a request to /products/123/page/2 is internally rewritten to /product.php?id=123&page=2. The user’s browser still shows the clean URL, but your PHP application receives the standard query parameters.

Add Trailing Slashes to URLs

Maintaining consistent URL formatting improves SEO by preventing duplicate content issues. This rule adds a trailing slash to URLs that lack one (excluding files with extensions):

server {
    listen 80;
    server_name example.com;
    
    # Add trailing slash to paths without file extensions
    rewrite ^([^.]*[^/])$ $1/ permanent;
}

The regex ^([^.]*[^/])$ matches paths that do not contain a dot (so it excludes .html, .php, etc.) and do not already end with a slash. A request to /about redirects to /about/, while /style.css passes through unchanged.

Remove PHP Extensions from URLs

Create clean URLs for PHP applications by internally adding the .php extension. This approach combines Nginx try_files directive behavior with a named location:

server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    index index.php index.html;
    
    location / {
        try_files $uri $uri/ @extensionless-php;
    }

    location @extensionless-php {
        rewrite ^(/[^.]*[^/])$ $1.php last;
    }
}

When a request like /contact arrives, Nginx first checks if the file exists literally, then tries it as a directory, and finally falls back to the @extensionless-php location. The rewrite adds .php and starts a new location search. Pair this pattern with the PHP-FPM location already used by the site, because socket paths and FastCGI include files vary by package source.

Prevent Image Hotlinking

Protect your bandwidth by blocking requests from unauthorized referrers. The valid_referers directive sets the $invalid_referer variable, which you can use to block or redirect hotlink attempts:

server {
    listen 80;
    server_name example.com;
    
    location ~* \.(gif|png|jpe?g|webp)$ {
        valid_referers none blocked server_names
                       *.example.com
                       ~\.google\.
                       ~\.bing\.;
        
        if ($invalid_referer) {
            return 403;
        }
    }
}

The valid_referers directive accepts several arguments:

  • none: Allows requests with no Referer header (direct visits, bookmarks)
  • blocked: Allows requests where the Referer was removed by a proxy or firewall
  • server_names: Allows your own domain names from the server_name directive
  • Additional domains or regex patterns for allowed referrers

Note the escaped dot (\.) in the regex pattern ~*\.(gif|png|jpe?g|webp)$. Without the backslash, a dot matches any character, which could match unintended paths like /imagespng.

Handle Legacy URL Structures

When migrating from another CMS or URL structure, old paths often need redirects to their new equivalents. This pattern handles a common WordPress-to-static migration scenario:

server {
    listen 80;
    server_name example.com;
    
    # Redirect old dated permalink structure
    location ~* "^/(\d{4})/(\d{2})/(\d{2})/(.+)$" {
        return 301 /blog/$4;
    }
    
    # Redirect category pages
    location ~* ^/category/(.+)$ {
        return 301 /topics/$1;
    }
}

The first rule converts /2024/01/15/article-title to /blog/article-title. The date components are captured in $1, $2, and $3 but discarded, while $4 captures the slug. Use the dedicated guide to redirecting URLs in Nginx for broader redirect method selection.

Test and Reload Nginx Rewrite Rule Changes

Before reloading Nginx with new rewrite rules, test the full configuration for syntax errors. A typo in a regex or missing semicolon can prevent Nginx from accepting the new file.

Validate Configuration Syntax

Run the configuration test command:

sudo nginx -t

A successful test produces this output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If there are errors, Nginx reports the file and line number:

nginx: [emerg] unknown directive "rewrit" in /etc/nginx/sites-enabled/example.com:15
nginx: configuration file /etc/nginx/nginx.conf test failed

Reload Nginx Safely

After confirming the configuration is valid, reload Nginx to apply changes without dropping existing connections:

sudo systemctl reload nginx

The reload command sends a signal to Nginx to re-read its configuration. Existing connections complete normally while new connections use the updated rules. If you need to fully restart (rarely necessary), use:

sudo systemctl restart nginx

Verify Nginx Redirects with curl

Test redirects with curl after the reload. The -I flag fetches response headers only, which is enough to confirm the status code and Location header. The curl command guide covers more header and redirect-testing options.

curl -I http://example.com/old-page

Relevant headers should show the redirect status and destination:

HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page

The Location header shows where Nginx is redirecting the request. To follow the redirect chain and see the final destination, add -L:

curl -IL http://example.com/old-page

Debug Nginx Rewrite Rules with Logs

When rewrite rules do not behave as expected, enable the rewrite log to see exactly how Nginx processes each request. This feature logs all rewrite operations to the error log at the notice level.

Enable Rewrite Logging

Add the rewrite_log directive inside your server block and set the error log level to notice:

server {
    listen 80;
    server_name example.com;
    
    error_log /var/log/nginx/example.com.error.log notice;
    rewrite_log on;
    
    rewrite ^/old-directory/(.*)$ /new-directory/$1 permanent;
}

After reloading Nginx, use tail -f to watch the error log while making test requests. Press Ctrl+C after the relevant rewrite lines appear. The tail command guide for log monitoring explains follow mode and line-count options in more detail.

sudo tail -f /var/log/nginx/example.com.error.log

The important fragments are the matched pattern and redirect target. Nginx prefixes the real lines with timestamps and process IDs; client address and host vary by test environment:

"^/old-directory/(.*)$" matches "/old-directory/page.html", client: 127.0.0.1, server: example.com, request: "GET /old-directory/page.html HTTP/1.1", host: "example.com"
rewritten redirect: "/new-directory/page.html", client: 127.0.0.1, server: example.com, request: "GET /old-directory/page.html HTTP/1.1", host: "example.com"

Disable rewrite logging in production once debugging is complete. Logging every rewrite operation adds overhead and can fill up disk space on high-traffic servers.

Troubleshoot Common Nginx Rewrite Errors

Fix ERR_TOO_MANY_REDIRECTS from a Rewrite Loop

A browser loop usually appears as this error:

ERR_TOO_MANY_REDIRECTS

The most common rewrite cause is a rule that matches its own output. Inside a location block, using last can start a new location search that lands in the same block again. Nginx stops after 10 internal rewrite cycles and returns an error.

Check the error log for an internal redirection cycle:

sudo tail -n 50 /var/log/nginx/error.log

Relevant log lines may look like this:

rewrite or internal redirection cycle while processing "/download/example.mp3", client: 203.0.113.10, server: example.com, request: "GET /download/example.mp3 HTTP/1.1", host: "example.com"

Change the looping location-level rule from last to break, then test and reload Nginx:

location /download/ {
    rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 break;
}
sudo nginx -t
sudo systemctl reload nginx

Match Query Strings in Nginx rewrite Logic

A rewrite pattern that includes ? will not match because rewrite checks the URI path, not the query string. The request may keep returning the original page instead of redirecting.

curl -I 'http://example.com/product.php?id=123'

If the redirect does not run, inspect query parameters through Nginx variables such as $arg_id. For a simple query-to-path redirect, return is clearer than rewrite:

location = /product.php {
    if ($arg_id ~ "^[0-9]+$") {
        return 301 /products/$arg_id;
    }
}

Test the configuration, reload Nginx, then verify that the response includes the new Location header.

Fix Case-Sensitive Nginx rewrite Matches

URI matching in Nginx is case-sensitive by default. If a mixed-case request fails to match, check the response first:

curl -I http://example.com/Old-Directory/page.html

Use a case-insensitive regex location with ~* when the whole location should match mixed-case input. For a standalone rewrite pattern, use a PCRE inline modifier such as (?i) only when a real legacy URL set requires mixed-case matching.

rewrite (?i)^/old-directory/(.*)$ /new-directory/$1 permanent;

Quote Nginx rewrite Patterns with Braces or Semicolons

If a regular expression contains } or ;, quote the whole pattern. Otherwise, nginx -t can report a parsing error before the server accepts the configuration.

nginx: [emerg] directive "rewrite" is not terminated by ";" in /etc/nginx/sites-enabled/example.com:18
nginx: configuration file /etc/nginx/nginx.conf test failed
rewrite "^/archive/([0-9]{4})/([0-9]{2})/(.+)$" /blog/$3 permanent;

Secure Nginx Rewrite Rules

Rewrite rules can affect security when they trust user-controlled input or expose private parameters. Review these checks before applying broad redirects on a live site.

Avoid Open Redirects

Never use user-supplied input directly in redirect destinations. An open redirect vulnerability allows attackers to craft URLs that redirect users to malicious sites while appearing to come from your domain:

# DANGEROUS - Never do this
rewrite ^/redirect/(.*)$ $1 redirect;

This rule would allow /redirect/https://evil.com to redirect users to an attacker’s site. Always use explicit destination URLs or validate input against an allowlist.

Avoid Leaking Sensitive Query Strings

Query strings can contain debug flags, campaign identifiers, session tokens, or internal parameters. When a redirect moves users to a public destination that should not receive those values, drop the old query string explicitly.

rewrite ^/old-campaign$ /new-landing-page? permanent;

The trailing ? prevents Nginx from appending the original request arguments. Keep query strings only when the destination page actually needs them.

Conclusion

A safer Nginx rewrite rule has a clear directive, a tested flag, and deliberate query-string behavior. Keep return for direct redirects, use rewrite only when captures matter, and use the guide to configure Nginx security headers when you are hardening the same server.

Share this guide

Help another Linux user troubleshoot faster

Share this guide with someone troubleshooting Linux systems or saving it for later.

Follow LinuxCapable

Want more LinuxCapable guides in Google?

Add LinuxCapable as a preferred source so Google can show our tutorials more often in Top Stories and mark them as preferred in AI Mode and AI Overviews when relevant.

Add LinuxCapable as a preferred source on Google
Search LinuxCapable

Need another guide?

Search LinuxCapable for package installs, commands, troubleshooting, and follow-up guides related to what you just read.

Found this guide useful?

Support LinuxCapable to keep tutorials free and up to date.

Buy me a coffeeBuy me a coffee
Before commenting, please review our Comments Policy.
Formatting tips for your comment

You can use basic HTML to format your comment. Useful tags currently allowed in published comments:

You type Result
<code>command</code> command
<strong>bold</strong> bold
<em>italic</em> italic
<a href="https://example.com">link</a> link
<blockquote>quote</blockquote> quote block

Add to the discussion

Questions, fixes, command output, and version notes help keep this guide current.

Verify before posting: