When configuring Nginx as a reverse proxy with multiple backend servers, a common requirement is implementing intelligent retry logic. The challenge arises when you need to:
- First attempt connection to upstream server A
- Fall back to server B if A fails
- Retry server A again after B's response
The standard Nginx proxy_next_upstream
directive typically handles simple failover scenarios, but doesn't provide the precise control needed for complex retry patterns. The key limitations include:
- No native support for retrying previously failed upstreams
- Limited conditional logic based on specific status codes
- Difficulty coordinating with external control processes
Here's an improved configuration that properly handles the retry flow:
upstream backend_a {
server 192.168.1.10:8080;
}
upstream backend_b {
server 192.168.1.20:8080;
}
server {
listen 80;
server_name example.com;
location / {
error_page 502 = @try_backend_b;
proxy_pass http://backend_a;
proxy_intercept_errors on;
}
location @try_backend_b {
error_page 502 = @retry_backend_a;
proxy_pass http://backend_b;
proxy_intercept_errors on;
# Optional: Add logic to contact control server
error_page 418 = @contact_control_server;
}
location @retry_backend_a {
proxy_pass http://backend_a;
}
location @contact_control_server {
proxy_pass http://127.0.0.1:82;
proxy_intercept_errors on;
# Control server response handling
error_page 200 = @retry_backend_a;
error_page 503 = @final_error;
}
location @final_error {
return 503 "Service Unavailable";
}
}
The solution works through several important mechanisms:
- Named locations create distinct processing blocks for each retry attempt
- error_page directives with equals sign (=) preserve the original request method
- proxy_intercept_errors enables custom handling of backend responses
For more sophisticated scenarios, consider integrating health checks:
upstream backend_group {
zone backend_zone 64k;
server 192.168.1.10:8080 max_fails=1 fail_timeout=10s;
server 192.168.1.20:8080 max_fails=1 fail_timeout=10s;
# Custom health check
health_check uri=/health interval=5s;
}
When integrating with your backend control system:
location @control_trigger {
proxy_pass http://control-server/start_backend;
proxy_set_header X-Original-URI $request_uri;
# Important for POST requests
proxy_method POST;
proxy_pass_request_body on;
# Response handling
proxy_intercept_errors on;
error_page 200 = @retry_original;
error_page 503 = @service_unavailable;
}
- Set appropriate timeouts (
proxy_connect_timeout
, proxy_read_timeout
)
- Limit retry attempts to prevent request loops
- Consider implementing caching for control server responses
When configuring Nginx as a reverse proxy with multiple backend servers, implementing a robust fallback mechanism where Nginx sequentially attempts upstream servers (A → B → A) presents unique challenges. The standard proxy_pass directive doesn't natively support this multi-stage retry behavior.
The conventional approach using error_page directives has limitations:
location @backend {
proxy_pass http://backend_a;
error_page 502 @try_backend_b;
}
location @try_backend_b {
proxy_pass http://backend_b;
error_page 502 @retry_backend_a;
}
This fails because:
- Nginx doesn't properly chain named locations for retries
- Status codes from intermediate handlers get passed to clients
- There's no clean way to implement conditional retry logic
The most reliable approach combines upstream blocks with proxy_next_upstream:
upstream backend_chain {
server backend_a:80 max_fails=0;
server backend_b:80 max_fails=0 backup;
server backend_a:80 backup;
}
server {
listen 80;
location / {
proxy_pass http://backend_chain;
proxy_next_upstream error timeout http_502;
proxy_intercept_errors on;
# Control server integration
error_page 502 = @start_backend;
}
location @start_backend {
proxy_pass http://control_server:82;
proxy_intercept_errors on;
# Successful startup (451 is just an example)
error_page 451 = @backend_chain;
# Control server failure
error_page 502 503 /fatal_error.html;
}
}
For more complex scenarios requiring conditional retries:
map $upstream_status $retry_strategy {
502 @start_backend;
default $upstream_response_status;
}
server {
# ... other config ...
location / {
proxy_pass http://backend_chain;
proxy_next_upstream error timeout http_502;
# Dynamic retry handling
error_page 502 = $retry_strategy;
}
}
Key implementation details:
- Set appropriate timeouts (proxy_connect_timeout, proxy_read_timeout)
- Consider health checks for backend servers
- Implement circuit breaking patterns to avoid cascading failures
- Log all retry attempts for debugging
For maximum flexibility with OpenResty:
location / {
access_by_lua_block {
local attempts = 0
local backends = {"http://backend_a", "http://backend_b"}
while attempts < 3 do
local res = ngx.location.capture("/proxy", {
args = { backend = backends[attempts % 2 + 1] }
})
if res.status < 500 then
ngx.exit(res.status)
end
attempts = attempts + 1
end
ngx.exit(502)
}
}
location /proxy {
internal;
proxy_pass $arg_backend;
}
Implementing Nginx Retry Logic: Proxy Fallback from Upstream A to B and Back to A
2 views