Nginx rewrite rules let you modify incoming URL requests before processing them. You can redirect users from old URLs to new ones, enforce HTTPS connections, create clean URLs for CMS platforms, and restructure your site without breaking existing bookmarks or search engine rankings. This guide covers both the return and rewrite directives with practical examples you can adapt for your own server configuration.
By the end of this guide, you will understand when to use return versus rewrite, how to write regex patterns for URL matching, and how to test and apply your configuration changes safely. Each example includes the complete server block context so you can see exactly where directives belong.
Prerequisites
Before configuring rewrite rules, ensure Nginx is installed and running on your server. If you need installation instructions, see our distribution-specific guides for installing Nginx on Ubuntu, Debian, or Fedora.
Verify Nginx is running before proceeding:
sudo systemctl status nginx
The output should show the service as active:
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Sat 2026-01-04 10:15:22 UTC; 2h ago
Docs: man:nginx(8)
Main PID: 1234 (nginx)
Tasks: 3 (limit: 4567)
Memory: 8.5M
CPU: 125ms
CGroup: /system.slice/nginx.service
├─1234 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
└─1235 "nginx: worker process"
Nginx configuration files are typically located in /etc/nginx/. Server blocks (virtual hosts) go in /etc/nginx/sites-available/ on Debian-based systems or /etc/nginx/conf.d/ on RHEL-based systems. The examples in this guide use the Debian-style layout, but the rewrite syntax works identically on all distributions.
Understand Return vs Rewrite Directives
Nginx provides two main directives for URL manipulation: return and rewrite. While they can achieve similar results, each has distinct use cases and performance characteristics. The following table summarizes when to choose each approach.
| Aspect | return Directive | rewrite Directive |
|---|---|---|
| Primary use | Simple redirects, status codes | Complex URL transformations |
| Regex support | No pattern matching | Full PCRE regex support |
| Performance | Faster (no regex processing) | Slightly slower (regex evaluation) |
| Capture groups | Cannot extract URL parts | Supports $1, $2, etc. |
| Internal rewrites | Always external redirect | Can rewrite internally with flags |
| Best for | Domain redirects, HTTPS enforcement | URL restructuring, clean URLs |
Use return when you need a straightforward redirect without pattern matching. Use rewrite when you need to capture parts of the URL or perform complex transformations. For more details on Nginx’s conditional logic, see our guide on Nginx if-else directives.
Redirect URLs with the Return Directive
The return directive stops request processing immediately and sends a response to the client. It accepts an HTTP status code and optionally a URL or response body. Because it does not evaluate regular expressions, return executes faster than rewrite for simple redirects.
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 redirects to https://newsite.com/products/widget. The 301 status code signals to search engines that they should transfer ranking authority to the new domain.
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):
location /admin {
# Deny access to admin area from external IPs
return 403;
}
location /api/v1 {
# Deprecation notice for old API version
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 Rewrite Directive
The rewrite directive modifies the request URI using PCRE (Perl Compatible Regular Expressions). Unlike return, it can capture parts of the URL and use them in the replacement string. The directive can either redirect externally or rewrite internally without the client seeing the change.
The syntax is:
rewrite regex replacement [flag];
The regex pattern matches against the normalized URI (without the query string). Captured groups are available as $1, $2, and so on in the replacement string. The optional flag controls what happens after the rewrite.
Understand Rewrite Flags
The flag parameter determines how Nginx processes the rewritten URL. According to the official Nginx rewrite module documentation, four flags are available:
last
- Stops processing the current set of rewrite directives
- Starts a new search for a matching location with the rewritten URI
- Use in server blocks or when you need location re-evaluation
break
- Stops processing rewrite directives in the current context
- Continues processing the request in the current location
- Use inside location blocks to prevent rewrite loops
redirect
- Returns a 302 temporary redirect
- The client sees the new URL in their browser
- Use when the replacement does not start with http://, https://, or $scheme
permanent
- Returns a 301 permanent redirect
- Browsers cache this redirect
- Search engines transfer ranking to the new URL
If the replacement string starts with
http://,https://, or$scheme, Nginx automatically performs an external redirect regardless of the flag. In this case, the flag only determines whether it returns 301 or 302.
Choose Between 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 Rewrite Rule Examples
The following examples demonstrate common URL transformation scenarios. Each includes the complete context so you can see exactly where the rules belong in your configuration.
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 on the location gives this block priority over regex locations. 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.
Convert Query Strings to Clean URLs
Transform dynamic URLs with query parameters into static-looking paths. This internal rewrite allows your application to receive the original query format while users see clean URLs:
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 uses try_files combined 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 ~ \.php$ {
# Your PHP-FPM configuration here
try_files $uri =404;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
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 re-evaluates, matching the PHP location block.
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 firewallserver_names: Allows your own domain names from theserver_namedirective- 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, you often need to redirect old paths to new ones. This example 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. For more redirect patterns, see our comprehensive guide on redirecting URLs in Nginx.
Test and Apply Configuration Changes
Before reloading Nginx with new rewrite rules, always test the configuration for syntax errors. A typo in a regex or missing semicolon can prevent Nginx from starting.
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 Redirects with curl
Test your rewrite rules using curl with the -I flag to fetch headers only:
curl -I http://example.com/old-page
For a 301 redirect, you should see:
HTTP/1.1 301 Moved Permanently Server: nginx/1.24.0 Date: Sat, 04 Jan 2026 10:30:00 GMT Content-Type: text/html Content-Length: 162 Connection: keep-alive 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 Rewrite Rules
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;
# Your rewrite rules here
}
After reloading Nginx, tail the error log while making test requests:
sudo tail -f /var/log/nginx/example.com.error.log
The log shows each rewrite step:
2026/01/04 10:45:12 [notice] 1234#1234: *1 "^/old-directory/(.*)$" matches "/old-directory/page.html", client: 192.168.1.100, server: example.com, request: "GET /old-directory/page.html HTTP/1.1", host: "example.com" 2026/01/04 10:45:12 [notice] 1234#1234: *1 rewritten data: "/new-directory/page.html", args: "", client: 192.168.1.100, 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.
Common Rewrite Problems
Infinite Redirect Loop (ERR_TOO_MANY_REDIRECTS)
This occurs when a rewrite rule matches its own output. Check if your rule inside a location block should use break instead of last. Nginx limits rewrites to 10 cycles before returning a 500 error.
Rewrite Rule Not Matching
Remember that the rewrite regex matches against the normalized URI without the query string. If your pattern includes ? or other query string elements, they will not match. Use the $args variable in an if condition to check query parameters:
if ($args ~ "^id=([0-9]+)") {
set $product_id $1;
rewrite ^/product\.php$ /products/$product_id? permanent;
}
The trailing ? in the replacement prevents Nginx from appending the original query string.
Case Sensitivity Issues
URI matching in Nginx is case-sensitive by default. If you need case-insensitive matching for a location block, use the ~* modifier instead of ~. For rewrite patterns, regex case sensitivity is determined by the pattern itself.
Security Considerations
Rewrite rules can have security implications if not carefully constructed. Keep these points in mind when configuring URL transformations.
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.
Preserve Query Strings Carefully
By default, Nginx appends the original query string to the rewritten URL. If your new URL should not include the old parameters, add a ? at the end of the replacement:
# Preserves query string (default)
rewrite ^/old$ /new permanent;
# /old?debug=1 becomes /new?debug=1
# Discards query string
rewrite ^/old$ /new? permanent;
# /old?debug=1 becomes /new
Preserving unexpected query strings can sometimes cause issues with the destination page or reveal internal parameters to users.
Conclusion
You now have the tools to redirect URLs, transform paths with regex captures, and debug rewrite behavior using Nginx’s built-in logging. The key principle is choosing return for simple redirects where performance matters and rewrite when you need pattern matching and URL transformation. For production servers, always test configuration changes with nginx -t before reloading, and consider configuring security headers alongside your rewrite rules for comprehensive protection.