RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
OpenResty Lua Upstream Module 502 Auditor
02. Root Cause Vector

OpenResty's Lua upstream module throws 502 Bad Gateway when upstream peer selection fails, connection pools are exhausted, or keepalive q...

03. Rapid Prescription

Verify upstream socket availability, proxy_pass hostnames, and increase proxy_read_timeout.

Zero-Trust Client IDE/ Performance

OpenResty Lua Upstream Module 502 Auditor

OpenResty's Lua upstream module throws 502 Bad Gateway when upstream peer selection fails, connection pools are exhausted, or keepalive queues are misconfigured, taking down production traffic instantly.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(73L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
---
# legacy-nginx-upstream-config.yaml
# WARNING: DO NOT EDIT - this is a legacy config from the PCI-SECURE-ZONE migration
# TODO: Refactor this mess after Q4 compliance audit

upstream legacy_backend {
    # VULNERABILITY: Lua upstream module timeout mismatch with proxy_read_timeout
    server my-registry.internal/legacy-api:8080 max_fails=3 fail_timeout=30s;
    keepalive 32;
    # OLD: server 10.2.34.56:8080 backup;  # commented out but might still be referenced somewhere
}

server {
    listen 443 ssl;
    server_name api.pci-secure-zone.corp;
    
    ssl_certificate     /etc/ssl/certs/wildcard.pem;
    ssl_certificate_key /etc/ssl/private/wildcard.key;
    
    # AWS keys for S3 logging - rotated quarterly (supposedly)
    set $aws_access_key "AKIAIOSFODNN7EXAMPLE";
    set $aws_secret_key "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
    
    location /v1/transactions {
        # Stripe sandbox token - TODO: move to vault
        set $stripe_token "sk_test_4eC39HqLyjWDarjtT1zdp7dc";
        
        access_by_lua_block {
            -- TODO: Fix this race condition with upstream health checks
            local upstream = require "ngx.upstream"
            local peers = upstream.get_primary_peers("legacy_backend")
            if not peers or #peers == 0 then
                ngx.log(ngx.ERR, "No upstream peers available")
                -- Fallback to old IP (might be dead)
                ngx.var.proxy_pass = "http://10.2.34.56:8080"
            end
        }
        
        proxy_pass http://legacy_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        
        # Mismatch: Lua upstream module uses 5s timeout but proxy_read_timeout is 60s
        # This causes 502 when Lua kills connection before proxy timeout
        proxy_read_timeout 60s;  # Global team mandate for PCI compliance
        
        # Embedded RSA key for legacy auth (should be in secrets manager)
        set $legacy_private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArGJ7HhE9u6Nf7qLzG9KcX8YvLm7tJwY8qPZ6aF3bV2nT\n... (truncated for brevity) ...\n-----END RSA PRIVATE KEY-----";
    }
    
    location /health {
        # Quick health check that bypasses Lua - might mask upstream issues
        proxy_pass http://legacy_backend/status;
        proxy_read_timeout 2s;
    }
}

# Database URI for audit logs (PostgreSQL)
# TODO: Encrypt this in transit
set $audit_db_uri "postgresql://audit_user:LegacyPassw0rd!@pg-audit.internal.pci:5432/audit_logs";

# Lua upstream module configuration
lua_package_path "/usr/local/lib/lua/5.1/?.lua;;";

init_by_lua_block {
    -- Global upstream config with aggressive timeouts
    upstream = require "ngx.upstream"
    upstream.set_timeouts("legacy_backend", { connect = 5000, send = 5000, read = 5000 })
    -- VULNERABILITY: This 5s read timeout conflicts with proxy_read_timeout 60s above
    -- When upstream responds slowly (>5s but <60s), Lua kills connection but proxy waits
    -- Result: 502 Bad Gateway with "upstream prematurely closed connection"
}
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing 502 Bad Gateway in OpenResty: Lua Upstream Module Errors Diagnosed and Resolved

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–45 mins

TL;DR

  • What broke: OpenResty's lua-resty-upstream or balancer_by_lua_block failed to select a valid peer — either the keepalive pool is exhausted, the upstream peer is marked down, or ngx.balancer was called outside a valid phase, returning 502 to every client.
  • How to fix it: Audit your balancer_by_lua_block logic, increase or properly release keepalive connections, reset failed peer state, and validate upstream health-check thresholds.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your nginx.conf and Lua upstream block and get a corrected diff without sending your config to any external server.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

Your logs look like this:

2024/01/15 03:42:17 [error] 1234#0: *9182 lua balancer: failed to get peer: no available peer, ...
client: 10.0.1.55, server: api.internal, request: "POST /v2/process HTTP/1.1",
upstream: "http://backend_pool/v2/process", host: "api.internal"
2024/01/15 03:42:17 [warn]  1234#0: *9182 upstream server temporarily disabled while connecting to upstream

Or the lua-resty-upstream-healthcheck variant:

[error] peers in upstream "backend_pool" are all marked down after 3 consecutive failures

Immediate consequence: Every request hitting that server {} block returns HTTP 502. OpenResty has no valid peer to proxy to. If you are running a single upstream group without fallback, this is a total service outage.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is not a security exploit — it is a cascading availability failure with the following blast radius:

  1. Keepalive pool exhaustion: lua-resty-upstream maintains a per-worker keepalive queue. If your Lua code calls upstream:set_keepalive() inconsistently — or never calls it on error paths — sockets leak. Under load, the pool hits pool_size and new connections are rejected, producing 502s even when backend servers are healthy.

  2. Peer marked-down death spiral: lua-resty-upstream-healthcheck uses a shared memory zone (lua_shared_dict) to track peer failure counts. If fail_timeout is too short and max_fails is too low (e.g., max_fails=1, fail_timeout=10s), a single slow backend response marks the peer down. With only 2–3 upstreams, one bad deploy marks all peers down simultaneously.

  3. balancer_by_lua_block phase violation: Calling ngx.balancer.set_current_peer() outside the balancer phase — or failing to call it at all on a code path — causes Nginx to have no peer set, which always produces 502. This is a silent logic bug that only surfaces under specific routing conditions.

  4. Cascading retry amplification: If proxy_next_upstream is set to retry on 502, a single bad upstream causes Nginx to fan out retries across all peers, multiplying backend load by your upstream count, accelerating the death spiral.


How to Fix It (The Solution)

Root Cause 1: Keepalive Pool Leak

Basic Fix — Always release the connection on every code path:

- local ok, err = upstream:set_keepalive()
- if not ok then
-     ngx.log(ngx.ERR, "failed to set keepalive: ", err)
-     -- BUG: falls through, socket leaked
- end

+ local ok, err = upstream:set_keepalive(60000, 100)  -- 60s timeout, pool size 100
+ if not ok then
+     ngx.log(ngx.ERR, "failed to set keepalive: ", err)
+     upstream:close()  -- CRITICAL: always close on keepalive failure
+ end

Enterprise Best Practice — Wrap all upstream calls in a pcall with guaranteed cleanup:

- local res, err = upstream:request(opts)
- if not res then
-     ngx.status = 502
-     ngx.exit(502)
- end

+ local ok, res_or_err = pcall(function()
+     return upstream:request(opts)
+ end)
+ if not ok or not res_or_err then
+     ngx.log(ngx.ERR, "upstream request failed: ", res_or_err)
+     upstream:close()           -- release socket unconditionally
+     ngx.status = 502
+     return ngx.exit(502)
+ end
+ local res = res_or_err

Root Cause 2: Peers Marked Down — Misconfigured Health Check Thresholds

  lua_shared_dict healthcheck 1m;

  init_worker_by_lua_block {
      local hc = require "resty.upstream.healthcheck"
      local ok, err = hc.spawn_checker({
          shm = "healthcheck",
          upstream = "backend_pool",
          type = "http",
          http_req = "GET /health HTTP/1.0\r\nHost: backend\r\n\r\n",
-         interval = 1000,       -- 1s: too aggressive, causes flapping
-         timeout = 500,
-         fall = 1,              -- marks down after 1 failure: too sensitive
-         rise = 1,
+         interval = 5000,       -- 5s: stable polling interval
+         timeout = 2000,        -- 2s: realistic backend timeout
+         fall = 3,              -- require 3 consecutive failures before marking down
+         rise = 2,              -- require 2 successes to mark back up
          valid_statuses = {200, 204},
          concurrency = 10,
      })
      if not ok then
          ngx.log(ngx.ERR, "healthcheck spawn failed: ", err)
      end
  }

Root Cause 3: `balancer_by_lua_block` Missing Peer Assignment

  upstream backend_pool {
      server 0.0.0.1;  -- placeholder, required by Nginx parser
      balancer_by_lua_block {
          local balancer = require "ngx.balancer"
          local upstream_obj = ngx.ctx.upstream

-         if upstream_obj.peer then
-             balancer.set_current_peer(upstream_obj.peer.host, upstream_obj.peer.port)
-             -- BUG: no else branch — Nginx has no peer if condition is false → 502
-         end

+         local peer = upstream_obj and upstream_obj.peer
+         if not peer then
+             ngx.log(ngx.ERR, "balancer: no peer available in context")
+             return ngx.exit(502)
+         end
+         local ok, err = balancer.set_current_peer(peer.host, peer.port)
+         if not ok then
+             ngx.log(ngx.ERR, "balancer: set_current_peer failed: ", err)
+             return ngx.exit(502)
+         end
+         -- set timeouts explicitly to avoid inheriting nginx defaults
+         balancer.set_timeouts(1, 5, 5)  -- connect, send, read (seconds)
      }
  }

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Lua Static Analysis in Pipeline

Add luacheck to your CI with a ruleset that catches missing close() calls:

# .luacheckrc
std = "ngx_lua"
ignore = {"611", "612"}
globals = {"ngx", "upstream", "balancer"}
# Flag any function that calls upstream:request without a paired close/set_keepalive
# .github/workflows/lint.yml
- name: Lua upstream lint
  run: |
    luarocks install luacheck
    luacheck lua/ --config .luacheckrc --fatal-warnings

2. Nginx Config Validation Gate

# Run in CI before every deploy
docker run --rm -v $(pwd)/nginx:/etc/nginx openresty/openresty:alpine \
    openresty -t -c /etc/nginx/nginx.conf

3. Shared Dict Sizing Check (OPA Policy)

If you use Terraform to provision OpenResty config maps, add an OPA/Conftest policy:

# policy/openresty.rego
package openresty

deny[msg] {
    input.lua_shared_dict_size_mb < 5
    msg := "lua_shared_dict for healthcheck must be >= 5MB to prevent peer state eviction under load"
}

4. Synthetic Canary on Upstream Health Endpoint

Deploy a Prometheus blackbox exporter probe against /health on each upstream. Alert at fall=2 equivalent — before OpenResty marks the peer down — giving you a 10-second window to intervene before 502s hit clients.

# prometheus/blackbox_targets.yml
- targets:
  - http://backend-1.internal:8080/health
  - http://backend-2.internal:8080/health
  labels:
    upstream_pool: backend_pool
    alert_threshold: "2_consecutive_failures"

Rule: If your synthetic probe fires and your fall counter hasn't triggered yet, your interval is too long. Tighten the probe interval, not the fall threshold.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does OpenResty return 502 even when my backend servers are running and healthy?
The root cause is that the `lua-resty-upstream-healthcheck` module has permanently marked all upstream peers as 'down' in its shared memory dictionary (`lua_shared_dict`) due to past transient failures. The health check worker, running in `init_worker_by_lua`, hasn't completed enough consecutive successful checks to meet the configured `rise` threshold and reinstate them, leaving your load balancer with zero viable targets. This state persists across worker restarts, creating a silent, self-inflicted outage where your infrastructure is up but your gateway is blind to it.
Q2.What is the difference between a 502 from `balancer_by_lua_block` versus a standard Nginx upstream 502?
Manually diagnosing this distinction is a high-risk trap. A standard 502 means Nginx connected but got garbage; a Lua balancer 502 means your script failed to provide any peer at all, often due to corrupted state in shared dicts or race conditions. Manually patching the Lua logic or tweaking timeouts in production is error-prone and introduces security vulnerabilities—like accidentally exposing admin endpoints or creating new race conditions. Instead of risking a blast radius, drop the broken Lua manifest or config snippet into the StackEngine Local Sandbox above. It will automatically compile, analyze, and generate a zero-trust, race-condition-free fix you can deploy immediately.
Q3.How do I safely drain and reset all peer states in `lua-resty-upstream-healthcheck` without restarting OpenResty?
Attempting this manually across a fleet is a compliance and operational nightmare. While you can flush dicts via a privileged endpoint, doing so safely requires airtight mTLS and network policies for every cluster. At scale, in a large mono-repo with hundreds of services, manually ensuring consistent, audited remediation is impossible. This is precisely why you need the StackEngine CI/CD Automator (Team Edition). It provides on-prem, automated pull request generation with Maker-Checker review, generating audited, policy-compliant patches for your entire infrastructure codebase, turning a chaotic security risk into a governed, automated workflow.