A 502 Bad Gateway means a proxy in your request path, such as nginx, a load balancer or a CDN edge, reached the next server upstream and got back an invalid response or no usable response at all. In practice, most 502s come from three causes: a refused connection, a connection reset, or a malformed or oversized response header. This playbook walks you from symptom to the failing hop and to a verified fix. Plan on 15 to 45 minutes for a typical incident.
Under RFC 9110 (2022), status 502 is reserved for a gateway or proxy that received an invalid response from an inbound server, while upstream timeouts belong to 504. In nginx, a default proxy_read_timeout of 60 seconds that expires produces a 504. A 502 that returns in under 100 ms almost always points to a refused, reset or garbled upstream connection rather than a slow one.
A 502 is always generated by an intermediary, never by the application that failed. The browser, an edge node, a regional load balancer and your nginx tier can each emit one. That makes the first diagnostic question "which hop wrote this response?" rather than "what is broken?" Answer that, and the search space drops from the whole stack to one connection.
Keep the neighboring codes separate. A 503 means the server or proxy is deliberately refusing work: overload, maintenance, or no healthy backends under some load balancers. A 504 means the upstream accepted the connection and then took too long. A 502 means the conversation with the upstream broke. Some vendors blur these lines. Cloudflare, for example, uses its own 520 to 526 range for origin failures. The mapping still holds as a starting hypothesis.
| nginx error log message | Likely cause | Fix |
|---|---|---|
| connect() failed (111: Connection refused) | Upstream not listening, crashed, or wrong port | Restart the process, fix the bind address, add a health check |
| upstream prematurely closed connection while reading response header | Keepalive race, or a worker killed mid-request | Lengthen the upstream idle timeout, check worker timeouts and OOM kills |
| recv() failed (104: Connection reset by peer) | Upstream or a middlebox reset the socket | Compare idle timeouts, inspect conntrack and firewall idle limits |
| upstream sent too big header while reading response header | Response headers exceed proxy_buffer_size | Raise the buffer to 16k, shrink cookies |
| connect() to the PHP-FPM socket failed (11: Resource temporarily unavailable) | FPM backlog full, pm.max_children reached | Size the pool to memory, raise the listen backlog, fix slow requests |
| no live upstreams while connecting to upstream | Every server marked failed by max_fails | Fix the root failure, then review max_fails and fail_timeout |
| SSL_do_handshake() failed while SSL handshaking to upstream | Protocol or cipher mismatch, missing SNI | Enable proxy_ssl_server_name, align TLS versions |
The error log message identifies the failing syscall, so read it before you touch any timeout or buffer setting.
The intermittent 502 that never reproduces on demand is usually a keepalive race. Picture a proxy holding an idle pooled connection, and an upstream that closes idle connections sooner than the proxy expects. When both act in the same millisecond, the proxy writes a request into a socket the upstream has already closed. That request fails.
The rule is simple. Each hop's idle timeout must be longer than the idle timeout of the hop that calls it. Defaults break this rule constantly. Node.js uses a server keepAliveTimeout of 5 seconds and Gunicorn a keepalive of 2 seconds. An AWS Application Load Balancer, by contrast, defaults to a 60-second idle timeout.
| Hop (outer to inner) | Setting | Example safe value |
|---|---|---|
| CDN edge to origin | Origin keepalive timeout (vendor setting) | Shortest value in the chain, for example 5 to 30 s |
| Load balancer | Idle timeout | 60 s |
| nginx tier | keepalive_timeout | 75 s (the nginx default) |
| Application server | Node keepAliveTimeout plus headersTimeout, or Gunicorn keepalive | 90 s, with headersTimeout above keepAliveTimeout |
Idle timeouts should grow as you move inward, so the caller always closes an idle connection before the server it calls does.
Between nginx and its upstreams, pooled connections only work when the upstream block declares a keepalive count. You also need to set proxy_http_version to 1.1 and clear the Connection header. Without those settings, nginx opens a new connection per request, which hides the race but adds a handshake to every call.
When a CDN reports a bad gateway, the edge could not complete a valid exchange with your origin or with its own shield tier. The most common culprits are an origin firewall that allowlists only some edge ranges, an expired or mismatched origin certificate, and an origin that sees the wrong Host header. Amazon CloudFront, for example, returns 502 when it cannot negotiate TLS with the origin.
Load is the other CDN-specific trigger. After a purge or a cold cache, thousands of edges can miss on the same object at once and open connections to an origin sized for a 95% hit ratio. That storm exhausts worker pools and produces exactly the 502s listed above. Origin shield and request coalescing collapse those concurrent misses into one upstream fetch. BlazingCDN includes both on every plan, alongside Prometheus metrics for watching origin error rates, as listed on the BlazingCDN features page for origin shield and request coalescing.
Change one setting per deploy and keep the previous config version alongside the new one. Before reloading, nginx must pass its configuration test. A reload is graceful, so in-flight requests finish on the old workers. If the 502 ratio rises within 15 minutes, restore the previous file and reload again. For load balancer and CDN settings, record the old values in the change ticket before editing, because vendor consoles rarely keep history.
Retries hide 502s from users but can multiply load during an incident. Since version 1.9.13, nginx no longer retries non-idempotent methods such as POST on the next upstream unless you add the non_idempotent flag. Leave it off unless your handlers are truly idempotent.
Longer idle timeouts hold more sockets and file descriptors open on the upstream. Larger header buffers raise memory per connection. Generous health check thresholds keep a flapping backend in rotation longer. Each of these settings trades failure visibility for smoothness, so pair every change with an alert on the underlying error.
A 502 Bad Gateway is a server-side error, generated by a proxy, load balancer or CDN that failed to get a valid response from the server behind it. Clearing the browser cache rarely helps. The one client-side exception is an oversized cookie that inflates response headers past the proxy buffer, which clearing site cookies can temporarily resolve.
A 502 Bad Gateway means the upstream connection broke or returned invalid data, while a 504 means the upstream accepted the request but answered too slowly. Timing usually separates them: 502s often return in milliseconds after a refused or reset connection. A 504 arrives close to the configured timeout, such as nginx's 60-second proxy_read_timeout default.
Intermittent 502 Bad Gateway errors usually come from a keepalive race, where the upstream closes an idle connection just as the proxy reuses it. They are common when an application keeps a short default, such as Node.js at 5 seconds, behind a load balancer idling at 60 seconds. Making each upstream's idle timeout longer than its caller's removes them.
Send the same request directly to the origin IP with the correct Host and SNI. If the origin returns 200, the 502 Bad Gateway comes from the path between CDN and origin: TLS, firewall allowlists or Host handling. If the origin also fails, the fault is yours. Vendor request ID headers and branded error pages also reveal which hop responded.
Write down the idle timeout of every hop between your CDN edge and your application process: the edge-to-origin keepalive, the load balancer, nginx, and the app server. Any hop that times out before its caller is a latent 502 source. Fix it, then replay low-rate traffic with gaps longer than the old value and confirm zero 502s across 1,000 requests. Add $upstream_status to your access logs so the next incident starts at step four instead of step one.
If some of your 502s trace back to a single CDN provider rather than your origin, BlazingCDN can run as a second CDN behind DNS with a weighted traffic split, and the BlazingCDN multi-CDN integration guide describes how that setup works.