The NGINX internal redirect module enables regex-based URI routing in request phases that come after the built-in rewrite directive. While rewrite runs exclusively in the rewrite phase — one of the earliest stages of request processing — the NGINX internal redirect module operates in the preaccess, access, precontent, or content phases. This means it can act on variables set by modules like map, geo, and authentication handlers that are simply unavailable during the rewrite phase.
If you manage NGINX servers and need routing decisions based on runtime conditions, such as authentication results or GeoIP data, the NGINX internal redirect module provides a clean, declarative syntax for phase-aware request routing without resorting to the problematic if directive.
How the Internal Redirect Module Works
When NGINX processes a request, it passes through a series of phases in a fixed order:
- Server rewrite phase —
rewritedirectives inservercontext - Find config phase — location matching
- Rewrite phase —
rewriteandreturndirectives inlocationcontext - Post-rewrite phase — internal redirect after URI change
- Preaccess phase — rate limiting, connection limits
- Access phase — IP-based access control, authentication
- Precontent phase —
try_filesprocessing - Content phase — response generation (
proxy_pass,fastcgi_pass,return)
The built-in rewrite directive operates in phase 3. The NGINX internal redirect module, however, registers handlers in phases 5 through 8. This means it can evaluate conditions and redirect requests after authentication, rate limiting, and access control have already set their variables.
After performing an internal redirect, the request returns to the server rewrite phase and proceeds through location matching again with the new URI. This is the same mechanism that try_files and the index module use internally.
Why Not Just Use rewrite?
The rewrite directive handles most URI transformations perfectly well. However, there are scenarios where the NGINX internal redirect module is the better choice:
- Routing based on
maporgeovariables: While map variables are technically lazy-evaluated, certain complex routing patterns benefit from explicit phase control - Post-authentication redirects: Redirect users to different backends based on their authentication status or role, after the access phase has completed
- Conditional routing without
if: NGINX’sifdirective inside location blocks is famously problematic. The internal redirect module providesif=andif!=parameters that evaluate conditions safely within dedicated phase handlers - Phase-aware routing: When you need a redirect to happen at a specific point in the request lifecycle, rather than always at the rewrite phase
Additionally, NGINX Plus offers its own commercial internal_redirect directive (since version 1.23.4), but it only accepts a simple URI — no regex matching, no phase selection, and no conditional logic. The open-source NGINX internal redirect module documented here is significantly more powerful.
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux
Install the GetPageSpeed repository and the module package:
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-internal-redirect
Then load the module by adding the following line to the top of /etc/nginx/nginx.conf, before any http block:
load_module modules/ngx_http_internal_redirect_module.so;
For more details and available versions, see the RPM module page.
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-internal-redirect
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
The internal_redirect Directive
Syntax:
internal_redirect [-i] pattern replacement [phase=<phase>] [flag=<flag>] [if=<condition> | if!=<condition>]
Default: none
Context: server, location
The directive takes a PCRE regular expression pattern and a replacement URI. When the pattern matches the current request URI, NGINX performs an internal redirect to the replacement URI. The replacement can contain regex capture group references ($1, $2, etc.) and NGINX variables.
Parameters
| Parameter | Description |
|---|---|
-i |
Perform case-insensitive regex matching |
pattern |
PCRE regular expression to match against the request URI |
replacement |
Target URI for the internal redirect. Supports capture groups and variables. Must start with / or @ |
phase= |
Request phase in which the redirect runs: preaccess (default), access, precontent, or content |
flag= |
Additional action: break, status_301, status_302, status_303, status_307, or status_308 |
if= |
Only redirect when the condition evaluates to a non-empty, non-zero value |
if!= |
Only redirect when the condition evaluates to empty or zero (negated condition) |
Phase Selection
The phase= parameter controls when the redirect rule is evaluated during request processing:
preaccess(default) — Runs before access control checks. Use this for general routing that does not depend on authentication results.access— Runs during the access phase, alongsideallow/denyandauth_basicdirectives.precontent— Runs just before content generation, after all access checks have passed.content— Runs during content generation. Use with care, as this phase is also whereproxy_pass,fastcgi_pass, andreturnoperate.
Flag Options
The flag= parameter modifies behavior after a successful match:
break— Stops processing subsequentinternal_redirectrules in the same phase and immediately performs the redirectstatus_301throughstatus_308— Instead of an internal redirect, sends an external redirect response to the client with the specified HTTP status code. The replacement URI becomes theLocationheader value.
Conditional Evaluation
The if= and if!= parameters accept any NGINX variable or complex expression. The condition is considered false when it evaluates to an empty string or the string "0". Everything else is considered true.
Practical Examples
Basic URI Migration
Redirect requests from an old URI structure to a new one, transparently:
server {
listen 80;
server_name example.com;
location /old-blog {
internal_redirect ^/old-blog/(.+)$ /articles/$1 phase=preaccess;
}
location /articles {
proxy_pass http://backend;
}
}
When a client requests /old-blog/my-post, NGINX internally redirects to /articles/my-post and serves the response from the /articles location. The client’s browser URL does not change.
Case-Insensitive Matching
Handle URIs where casing is unpredictable, such as links shared on social media. Note that the -i flag only affects the internal_redirect regex — you must also use a case-insensitive regex location (~*) to ensure NGINX routes all casing variants into the same location block:
location ~* ^/products {
internal_redirect -i ^/products/(.+)$ /shop/$1 phase=preaccess;
}
location /shop {
proxy_pass http://catalog-backend;
}
The ~* location matches /Products/Widget, /PRODUCTS/widget, and /products/Widget equally. The -i flag ensures the internal_redirect regex also matches regardless of case.
Named Location Redirect
Route requests to a named location for specialized processing:
location /api {
internal_redirect ^/api/v1/(.+)$ @api_handler phase=preaccess;
}
location @api_handler {
internal;
proxy_pass http://api-backend;
}
Named locations (prefixed with @) are only accessible via internal redirects, providing an extra layer of isolation.
Conditional Redirect Based on a map Variable
Use map to set a routing variable, then act on it with internal_redirect:
map $uri $route_target {
default "";
~^/promo/.+ /campaigns;
}
server {
listen 80;
server_name example.com;
location /promo {
internal_redirect ^/promo/(.+)$ $route_target/$1 phase=preaccess if=$route_target;
}
location /campaigns {
proxy_pass http://campaigns-backend;
}
}
The redirect only fires when $route_target is non-empty. If the map does not match, the condition evaluates to false and the request continues normally.
Multiple Rules with break Flag
Process multiple redirect rules and stop at the first match using flag=break:
location / {
# Check rules in order; stop at first match
internal_redirect ^/legacy/admin(.*)$ /admin-panel$1 phase=preaccess flag=break;
internal_redirect ^/legacy/api(.*)$ /api/v2$1 phase=preaccess flag=break;
internal_redirect ^/legacy/(.*)$ /modern/$1 phase=preaccess;
}
Without flag=break, all matching rules in the same phase execute sequentially, and the last match determines the final redirect URI. With break, processing stops immediately after the first successful match.
External Redirect with Status Code
Use flag=status_301 or flag=status_302 to send a client-visible redirect instead of an internal one:
location /old-site {
internal_redirect ^/old-site/(.*)$ https://new.example.com/$1
phase=preaccess flag=status_301;
}
This sends an HTTP 301 Moved Permanently response with the Location header set to https://new.example.com/...`. The client's browser follows the redirect. This is functionally similar torewrite … permanent` but runs in the preaccess phase.
Negated Condition with if!=
Redirect all requests except when a specific condition is true:
map $remote_addr $is_internal {
default "0";
~^10\..+ "1";
~^192\.168\..+ "1";
}
server {
listen 80;
server_name example.com;
location /admin {
# Redirect non-internal users to the login page
internal_redirect ^/admin(.*)$ /login phase=preaccess if!=$is_internal;
proxy_pass http://admin-backend;
}
location /login {
proxy_pass http://auth-backend;
}
}
When $is_internal is "0" (non-internal IP), the if!= condition is true and the redirect fires. Internal users proceed directly to the admin backend.
Query String Handling
The NGINX internal redirect module matches the regex pattern against the full URI including query string arguments. When the request has query parameters, the module constructs the match string as $uri?$args.
For example, a request to /search?q=nginx&page=2 is matched against the string /search?q=nginx&page=2. Therefore, if you want to match a URI that may have query parameters, account for the ? in your regex:
# Match /search with or without query string
location /search {
internal_redirect "^/search(\?.+)?$" /new-search$1 phase=preaccess;
}
When the replacement URI contains a query string, it is parsed and set as the new $args. If you redirect to a URI without a query string, the original arguments are discarded.
Important: Phase Ordering with return
A common mistake is placing a return directive in the same location as internal_redirect. The return directive executes during the rewrite phase, which comes before any of the phases where internal_redirect operates. Therefore, return always takes precedence:
# WRONG: return always wins because it runs in the rewrite phase
location /example {
internal_redirect ^/example$ /target phase=preaccess;
return 200 "This always runs\n";
}
If you need a fallback response when the redirect does not fire, use try_files or proxy_pass instead of return:
# CORRECT: use a content-phase handler as fallback
location /example {
internal_redirect ^/example$ /target phase=preaccess if=$should_redirect;
try_files $uri =404;
}
This behavior is consistent with how NGINX request phases work — earlier phases always execute before later ones, regardless of directive order in the configuration file.
Infinite Loop Protection
NGINX has built-in protection against infinite redirect loops. Each request is limited to 10 internal redirects (controlled by the uri_changes counter in the NGINX core). If the limit is exceeded, NGINX returns a 500 Internal Server Error and logs a message:
rewrite or internal redirection cycle
Additionally, the NGINX internal redirect module avoids unnecessary redirects when the replacement URI matches the current URI. If the regex matches but the replacement evaluates to the same URI, the module updates only the query string arguments (if different) and allows the request to continue without restarting the phase cycle.
Troubleshooting
“unknown directive” Error
If nginx -t reports unknown directive "internal_redirect", the module is not loaded. Verify the load_module line is present at the top of nginx.conf:
load_module modules/ngx_http_internal_redirect_module.so;
Also verify the module file exists:
ls /usr/lib64/nginx/modules/ngx_http_internal_redirect_module.so
Redirect Not Firing
If the redirect does not seem to take effect:
- Check for
returnorrewrite ... lastin the same location — these run beforeinternal_redirectand may short-circuit the request - Verify the regex pattern — use
nginx -tto confirm there are no syntax errors. Test your regex separately with a tool likepcre2grep - Check the
if=condition — add a temporaryadd_header X-Debug-Var $your_variable always;to verify the variable’s value at request time - Check phase ordering — if you use
phase=contentand another content handler likeproxy_passis also present, they may conflict
Unexpected 500 Errors
A 500 Internal Server Error with the message rewrite or internal redirection cycle in the error log means the redirect is looping. Common causes:
- The replacement URI matches the same location, which triggers the same
internal_redirectrule again - Two locations redirect to each other
Resolve this by ensuring the replacement URI routes to a different location or by adding a condition to prevent re-matching.
Performance Considerations
The NGINX internal redirect module adds minimal overhead. Each internal_redirect directive compiles its regex pattern at configuration load time, so the runtime cost is limited to regex matching during request processing. For most configurations, this is negligible compared to network I/O and backend processing time.
However, each successful internal redirect causes NGINX to restart request processing from the server rewrite phase. This means all phase handlers run again for the new URI. Avoid chaining multiple internal redirects when a single redirect can achieve the same result.
Conclusion
The NGINX internal redirect module fills an important gap in NGINX’s request processing model. By operating in later request phases, it enables routing decisions that the built-in rewrite directive simply cannot make. Whether you need to redirect based on authentication results, map variables, or GeoIP data, this module provides a clean, declarative syntax for phase-aware request routing.
The module source code is available on GitHub. For pre-built packages, visit the GetPageSpeed module page.

