In high-concurrency microservice architectures, backend instances often suffer from transient memory pressure, full JVM Garbage Collection (GC) pauses, or database pool exhaustion. Under standard Nginx passive health checking (max_fails=3 fail_timeout=10s), dead or lagging nodes are only detected AFTER real client requests suffer connection timeouts or HTTP 502/503 errors.
During a database lock contention incident, 2 out of 10 backend pods began intermittently timing out. Passive checking failed to isolate the nodes quickly enough, resulting in 8,400 user requests failing before the proxy temporarily ejected the flapping pods.
# Passive Health Check Latency Lag 2026/08/04 21:10:02 [error] 18291#0: *492012 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 172.16.2.14, upstream: "http://10.0.20.11:8080/api/v1" 2026/08/04 21:10:04 [warn] 18291#0: *492018 [upstream_outlier] host 10.0.20.11:8080 ejected from pool for 30s Result: 8,400 real user requests dropped during ejection delay!
To achieve zero-downtime high availability, edge proxies must combine Passive Outlier Ejection with Active Background Probing.
┌─────────────────────────────────────────────────────────────┐
│ OpenResty Worker (lua_shared_dict healthcheck_zone) │
│ ├── Background Timer ──> Send Synthetic GET /healthz │
│ ├── Host A (10.0.20.10): 200 OK ──> Status: UP │
│ └── Host B (10.0.20.11): Timeout ──> Eject Node BEFORE User │
└──────────────────────────────┬──────────────────────────────┘
│
[ Clean User Traffic Route Only to UP Hosts ]
│
┌──────────────────┴──────────────────┐
▼ ▼
[ Node A: Healthy ] [ Node B: Ejected ]
While passive checks observe live user traffic, active background timers continuously dispatch lightweight HTTP GET /healthz probes to every upstream host independently. If a node fails $N$ consecutive synthetic checks or its p99 response time exceeds a threshold, OpenResty removes the host from the active load-balancing ring BEFORE user traffic hits it!
Deploy active probing and consecutive failure ejection inside OpenResty:
http {
# OpenResty Healthcheck Shared Memory Zone
lua_shared_dict healthcheck_zone 10m;
upstream backend_pool {
server 10.0.20.10:8080 max_fails=2 fail_timeout=5s;
server 10.0.20.11:8080 max_fails=2 fail_timeout=5s;
server 10.0.20.12:8080 backup;
keepalive 64;
}
# Initialize Active Health Checks in Worker Background
init_worker_by_lua_block {
local hc = require "resty.upstream.healthcheck"
local ok, err = hc.spawn_checker{
shm = "healthcheck_zone",
upstream = "backend_pool",
type = "http",
http_req = "GET /healthz HTTP/1.0\r\nHost: internal.health\r\n\r\n",
interval = 2000, -- Check every 2 seconds
timeout = 1000, -- 1s probe timeout
fall = 3, -- 3 consecutive failures = eject
rise = 2, -- 2 consecutive successes = restore
valid_statuses = {200, 302},
concurrency = 10,
}
}
server {
listen 80 reuseport;
server_name ha.zhabrosima.com;
location / {
proxy_pass http://backend_pool;
# Fast failover to next upstream on error
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 3s;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# Health Check Status Dashboard
location /upstream_status {
access_by_lua_block {
local hc = require "resty.upstream.healthcheck"
ngx.say(hc.status_page())
}
}
}
}
Inspect real-time upstream node health states using CLI commands:
# Query OpenResty live health check status page
curl -s http://127.0.0.1/upstream_status
# Expected Output:
# Upstream backend_pool
# Primary Peers:
# 10.0.20.10:8080 UP
# 10.0.20.11:8080 DOWN (3/3 fall checks failed)
When a failed backend node finishes garbage collection or re-establishes its database pool, returning it immediately to full load can trigger instant re-failure (Thundering Herd effect).
DOWN back to UP.We simulated intermittent node failures under 50,000 QPS load:
| Health Checking Strategy | Failed User Requests during Node Failure | Average Node Ejection Delay | Recovery Time |
|---|---|---|---|
| Passive Only (max_fails=3) | 8,420 Failed Requests | 10.2 seconds | 10.0 seconds |
| Active Probing + Passive Ejection | 0 Failed Requests | 0.4 seconds (Pre-empted) | 2.0 seconds |
| Performance Gain | 100% Error Elimination | -96.0% Ejection Delay | Instant Restoration |
nginx_upstream_peers_status{state="up"}rate(nginx_http_requests_total{status=~"502|503"}[1m])