Site icon GetPageSpeed

NGINX Server Redirect Module: Internal Routing

NGINX Server Redirect Module: Internal Virtual Server Routing Without External Redirects

The Problem: Routing Between Server Blocks Without External Redirects

You have multiple NGINX server blocks on the same machine. You need to route a request from one server block to another — perhaps for A/B testing, maintenance mode, or a migration. The NGINX server redirect module solves this exact problem, but first, consider what native NGINX offers:

None of these can internally switch the request to a different server {} block. You are stuck choosing between client-visible redirects and loopback network overhead.

The Solution: How the NGINX Server Redirect Module Works

The NGINX server redirect module performs internal server block switching within a single request, completely transparent to the client. The request is processed as if it originally arrived at the target server — no round trips, no exposed URLs, and no redirect chains visible in the browser.

The module registers a handler in the NGX_HTTP_POST_READ_PHASE — the very first phase after NGINX reads the client request. At this point, the module:

  1. Evaluates any server_redirect rules in the current server block.
  2. If a rule matches (unconditionally or via a condition), it looks up the target server name in NGINX’s virtual host hash table.
  3. If found, it switches the request’s server configuration, location configuration, and error log to the target server’s settings.
  4. The $server_redirect_original_host variable is set to preserve the original Host header value.
  5. The handler re-runs on the new server’s configuration, allowing chained redirects.

Because this happens before location matching, all location {} blocks, access controls, and content handlers in the target server block apply as if the request arrived there directly. This makes it fundamentally different from every native NGINX mechanism listed above.

Learn more about NGINX server setup to understand how server blocks work.

Processing Phase and Variable Availability

Because the NGINX server redirect module runs in the post-read phase, it executes before the rewrite phase. This means that set, map, and if directives have not yet been evaluated when server_redirect conditions are checked.

Only variables that are available at the post-read phase can be used in if= conditions. These include:

Variables set by set, map, or if directives are not available and will always evaluate to empty.

Loop Protection

The module tracks redirect depth per request. If more than 3 internal redirects occur (for example, server A redirects to B, B redirects to A, and so on), NGINX returns a 500 Internal Server Error and logs:

server redirect: too many redirects

This prevents infinite loops in misconfigured redirect chains.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux, Fedora

Install the NGINX server redirect module from the GetPageSpeed RPM repository:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-server-redirect

Then load the module in your /etc/nginx/nginx.conf (before any http {} block):

load_module modules/ngx_http_server_redirect_module.so;

Reload NGINX to activate:

sudo nginx -t && sudo systemctl reload nginx

Configuration Reference

Directive: server_redirect

Property Value
Syntax server_redirect <target_host> [if=<condition>] or [if!=<condition>]
Default
Context server

Redirects the current request internally to another server block identified by target_host. The target server must share the same listening port as the current server.

The target_host should be a concrete hostname that matches a server_name in another server block. Even if the target server uses a wildcard (*.example.com) or regex server_name, you must specify the actual hostname the request should resolve to.

If the target server cannot be found, the request falls through to the default server for that listen address.

You can specify multiple server_redirect directives in a single server block. They are evaluated in order, and the first matching rule takes effect.

Conditional Redirection with if=

The optional if=<condition> parameter makes the redirect conditional. The condition is an NGINX variable expression evaluated at runtime:

This follows the same truthiness convention as NGINX’s native if directive.

Negative Conditions with if!=

The if!=<condition> syntax inverts the logic — the redirect occurs when the condition is false (empty or “0”) and is skipped when the condition is true.

Directive: schedule_redirect

Property Value
Syntax schedule_redirect on \| off
Default schedule_redirect off
Context server

Enables path-based server redirection. When turned on, the module extracts the first path segment from the request URI and treats it as a target hostname.

For example, consider a request to gateway.example.com with the path /backend.example.com/api/v1/users. The module extracts backend.example.com from the first path segment and internally redirects to that server block. The URI becomes /api/v1/users and query strings are preserved.

After redirection, even $request_uri reflects the rewritten path. The only way to see the original full URI is through the $request variable, which contains the raw request line.

If server_redirect rules also exist in the same server block and their conditions are met, server_redirect takes priority and executes first.

Variable: $server_redirect_original_host

Stores the value of the Host header before the most recent redirect. This is useful for logging, passing to upstream backends, or setting response headers.

Important: In a chain of redirects (A → B → C), this variable holds the host from the previous hop (B), not the original requester (A). If you need to track the very first origin across multiple hops, capture $server_redirect_original_host at the intermediate server and pass it via a custom header.

Practical Use Cases

Use Case 1: Unconditional Server Redirect

Problem: You are migrating from old-api.example.com to new-api.example.com but cannot update all clients at once. You need traffic to the old hostname handled by the new server block without client-visible redirects.

Solution:

server {
    listen 80;
    server_name old-api.example.com;

    server_redirect new-api.example.com;
}

server {
    listen 80;
    server_name new-api.example.com;

    add_header X-Original-Host $server_redirect_original_host;

    location / {
        proxy_pass http://api_backend;
    }
}

The client sends requests to old-api.example.com, but they are processed entirely by the new-api.example.com server block. The client never sees a redirect.

Use Case 2: Conditional Routing Based on Headers

Problem: You want to run canary deployments where requests with a specific header go to a test backend, but all other traffic hits the stable backend. Using separate DNS records would require client-side changes.

Solution:

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

    # Route canary traffic to the canary server
    server_redirect canary.api.example.com if=$http_x_canary;

    location / {
        proxy_pass http://stable_backend;
    }
}

server {
    listen 80;
    server_name canary.api.example.com;

    location / {
        proxy_pass http://canary_backend;
    }
}

Requests with the X-Canary header (set to any non-empty, non-zero value) go to the canary backend. All other requests hit the stable backend.

Use Case 3: Multiple Conditional Rules

Problem: You need to route traffic to different backends based on multiple criteria — API version, beta access, or feature flags — without creating complex if chains inside location blocks.

Solution: Define multiple redirect rules. The first matching rule wins:

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

    server_redirect v2.app.example.com if=$http_x_api_version_2;
    server_redirect beta.app.example.com if=$http_x_beta;

    location / {
        proxy_pass http://production;
    }
}

Use Case 4: Negative Conditions for Maintenance Mode

Problem: You want to show a maintenance page to all users except those with a bypass header (for testing the deployment before going live).

Solution: Use if!= to redirect all traffic except when a bypass header is present:

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

    # Redirect to maintenance unless bypass header is set
    server_redirect maintenance.example.com if!=$http_x_bypass_maintenance;

    location / {
        proxy_pass http://app_backend;
    }
}

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

    location / {
        return 503 "Service temporarily unavailable. Please try again later.\n";
    }
}

Only requests with the X-Bypass-Maintenance header reach the application backend. Everyone else sees the maintenance page.

Use Case 5: Path-Based Gateway with schedule_redirect

Problem: You want a single entry-point server that dispatches requests to different backend servers based on the URL path, without maintaining a large location block map or using proxy_pass for each route.

Solution: Use schedule_redirect to build an API gateway:

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

    schedule_redirect on;

    # Requests without a valid first-path hostname land here
    location / {
        return 400 '{"error": "Missing or invalid service name in path"}\n';
    }
}

server {
    listen 80;
    server_name users.internal;

    location / {
        proxy_pass http://users_service;
    }
}

server {
    listen 80;
    server_name orders.internal;

    location / {
        proxy_pass http://orders_service;
    }
}

With this configuration:

Use Case 6: Chained Redirects

Problem: Your routing logic is layered — an entry point dispatches to a router, and the router conditionally dispatches to a final destination. You need multi-hop internal routing.

Solution: The module supports chained redirects (up to 3 hops):

server {
    listen 80;
    server_name entry.example.com;
    server_redirect router.example.com;
}

server {
    listen 80;
    server_name router.example.com;
    server_redirect final.example.com if=$http_x_route;
    location / { return 200 "router fallback\n"; }
}

server {
    listen 80;
    server_name final.example.com;
    location / { return 200 "final destination\n"; }
}

A request to entry.example.com with the X-Route header traverses two internal redirects and reaches final.example.com. Remember that the maximum chain depth is 3 redirects.

Performance Considerations

The NGINX server redirect module has minimal performance overhead because:

For high-traffic deployments, this module is significantly more efficient than the common workaround of using proxy_pass to 127.0.0.1 to route between virtual servers on the same machine.

Security Best Practices

Use Access Controls in the Target Server

Since server_redirect conditions can only use variables available at the post-read phase (such as request headers), you cannot combine conditions using set or if directives. Instead, apply security controls in the target server block where the full rewrite and access phases are available:

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

    # Redirect when the admin header is present
    server_redirect admin.example.com if=$http_x_admin;

    location / {
        proxy_pass http://public_backend;
    }
}

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

    # Security controls in the target server
    allow 10.0.0.0/8;
    deny all;

    location / {
        proxy_pass http://admin_backend;
    }
}

This approach ensures that even if a client sends the X-Admin header, they must also pass the IP-based access control in the target server block. The layered security happens at the right processing phase.

Restrict schedule_redirect Carefully

The schedule_redirect directive accepts the target hostname from the request URI. While the module validates the hostname and only redirects to existing server blocks (falling back to the default server otherwise), you should:

  1. Ensure your default server block has appropriate security controls — it will handle requests with unrecognized hostnames.
  2. Consider whether exposing internal server names in the URL path is acceptable for your use case.
  3. Use schedule_redirect on dedicated gateway server blocks, not on public-facing application servers.

Preserve Original Host Information

When proxying to upstream backends, the original host information is valuable for logging and debugging. Pass it along using the $server_redirect_original_host variable:

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

    location / {
        proxy_set_header X-Original-Host $server_redirect_original_host;
        proxy_set_header Host $host;
        proxy_pass http://upstream;
    }
}

Troubleshooting

“unknown directive server_redirect”

The module is not loaded. Verify that load_module modules/ngx_http_server_redirect_module.so; appears before the http {} block in your nginx.conf, and confirm the module file exists:

ls /usr/lib64/nginx/modules/ngx_http_server_redirect_module.so

Redirect Not Happening

  1. Check the listen port: The target server must share the same listen directive as the source server. A server on port 80 cannot redirect to a server listening on port 8080.
  2. Check the server_name: The target_host must exactly match a server_name in another server block (not a wildcard pattern).
  3. Check the condition variable: If using if=, remember that only post-read phase variables work. Variables set via set or map directives are not yet available and will always be empty. Use request header variables like $http_x_something instead.

To debug redirects, enable info-level logging:

error_log /var/log/nginx/error.log info;

The module logs each redirect at the info level with the target host:

server redirect: redirect to new server with host target.example.com

Check the log to see whether redirects are firing:

grep "server redirect:" /var/log/nginx/error.log

500 Internal Server Error

If you see server redirect: too many redirects in the error log, you have a redirect loop. Check that server A does not redirect to B while B redirects back to A. Review the redirect chain and ensure it terminates.

$server_redirect_original_host Is Empty

This variable is only populated after a redirect occurs. If the request was not redirected (it arrived directly at the target server), the variable is empty. This is expected behavior and can be used to distinguish redirected from direct requests.

Compatibility Notes

Conclusion

The NGINX server redirect module provides a clean, efficient mechanism for internal routing between virtual server blocks. Whether you need unconditional migration redirects, conditional A/B routing, maintenance mode switches, or path-based API gateway patterns, this module handles it without the latency and complexity of external redirects or loopback proxy connections.

The module is available as a dynamic module package from the GetPageSpeed repository. Source code and issue tracking are on GitHub.

You can use the NGINX config checker to validate your configuration after adding server redirect rules. Additionally, consider exploring the nginx-mod build for a feature-rich, optimized NGINX build that supports this and many other NGINX modules.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version