Skip to main content
DevOps Insightsguides & answers

HTTP · troubleshooting

502 Bad Gateway

The proxy could not get a usable response from the upstream server behind it, and is reporting a fact about somebody else. The answer is usually in whether the upstream was reachable, alive and speaking HTTP, though some causes below are proxy settings, such as a wrong upstream address or a header buffer that is too small.

First published

New to this? Read what is actually happening first, then come back to the steps.

On this page · 10 sections

Before you start

  • Read access to nginx's error log. The error_log directive sets where it goes (default logs/error.log under nginx's prefix); nginx -T shows the configured path.
  • A shell on the machine or in the container where nginx runs, because every reachability test has to start from the proxy, not from your laptop.
  • Root or sudo to run nginx -T and nginx -s reload, and permission to reload a production proxy.
  • The address, port and scheme (http or https) the application really listens on, from its own deployment config.
  • curl on the proxy host, or in the proxy container, for direct tests.

Run this first

step 1 of 3
sudo tail -50 /var/log/nginx/error.log

Look for connect() failed (111) = nothing listening · prematurely closed = upstream died mid-request · too big header = buffer · no live upstreams = whole pool ejected.

This is the step that decides everything else. The proxy already knows what went wrong and has written it down, and the distinct messages map almost one-to-one onto the causes below.

Work out which cause you have

A few questions to narrow the list. Every answer ends in a command that confirms or rules the cause out — this cannot see your cluster, so nothing here is a certainty until you have checked.

Narrow it down

0 answered · nothing is sent anywhere

What does the proxy's error log say?

The distinct messages map almost directly onto distinct causes, which is why this is first.

Cause space

6 of 6 still possible

  • Nothing is listening at the address the proxy is usingCommon
  • The upstream accepted the connection and then diedCommon
  • The proxy is configured for the wrong addressCommon
  • The upstream is speaking a different protocol than the proxy expectsOccasional
  • The upstream's response headers exceed the proxy's bufferOccasional
  • Every upstream in the pool has been marked unavailableOccasional

Nothing ruled out yet. Answer the question above and the branches your answer eliminates will strike through here.

Check it with a tool

Or diagnose it manually

In this order. The first command usually contains the whole answer.

  1. Step 1

    sudo tail -50 /var/log/nginx/error.log

    This is the step that decides everything else. The proxy already knows what went wrong and has written it down, and the distinct messages map almost one-to-one onto the causes below.

    Look for connect() failed (111) = nothing listening · prematurely closed = upstream died mid-request · too big header = buffer · no live upstreams = whole pool ejected.

  2. Step 2

    curl -sv http://<upstream-address>:<port>/ -o /dev/null

    Run from wherever the proxy runs. This separates "the proxy cannot reach it" from "the application is broken", which are different problems with the same status code.

    Look for A refusal means reachability. A valid response means the proxy config or protocol is the fault.

  3. Step 3

    sudo nginx -T 2>/dev/null | grep -B2 -A6 proxy_pass | head -40

    Prints the full configuration nginx would load from disk, with every include expanded, so you see the upstream that is really configured rather than the one file you remember editing. Reload after changes.

    Look for The upstream address and scheme, checked against where the application really listens.

Every cause, and how to fix it

Ordered by how often each one turns out to be the answer.

Nothing is listening at the address the proxy is using

Common

The upstream process is not running, is still starting, or is listening somewhere other than where the proxy is looking. The proxy's connection is actively refused, which is a fast failure rather than a hang — the 502 arrives immediately.

Confirm

sudo tail -50 /var/log/nginx/error.log | grep -iE 'connect\(\) (to [^ ]+ )?failed'

(111: Connection refused). That is the kernel saying nothing holds the port, which is unambiguous — the address in the message is where the proxy tried.

Fix

  • Confirm the upstream is running and listening on exactly the address in the proxy config, with ss -tlnp | grep <port>.
  • Check the bind address, not just the port. A process on 127.0.0.1:3000 is unreachable from a proxy in a different container or on another host, even though the port is correct.
  • If the upstream is a container, check the proxy is using the container name and container port rather than a published host port.
ss -tlnp | grep ':3000'   # is anything listening, and on what address?
curl -sv http://127.0.0.1:3000/ -o /dev/null   # from the proxy's own host

Run the curl from wherever the proxy runs. Reachability from your laptop proves nothing about reachability from the proxy.

The upstream accepted the connection and then died

Common

The connection succeeded, so this is not a reachability problem. The application then crashed, was killed, or closed the connection before sending a complete response — most often an unhandled exception in a worker, or the process being OOM-killed while handling the request.

Confirm

sudo tail -50 /var/log/nginx/error.log | grep -iE 'prematurely closed|recv\(\) failed'

upstream prematurely closed connection while reading response header. This is the signature: the proxy got a socket and then got nothing. If the upstream reset the connection instead of closing it, the line reads recv() failed (104: Connection reset by peer) while reading response header from upstream.

Fix

  • Look at the upstream's own logs at the timestamp of the 502. A crash usually leaves a stack trace, and this cause is the one where the application has the most to say.
  • Check whether the process was killed rather than crashed. Exit code 137 or an OOM entry means the request pushed it over a memory limit.
  • If it is intermittent and correlates with load, suspect worker exhaustion or a per-request memory spike rather than a logic bug.

The proxy is configured for the wrong address

Common

A hostname that no longer resolves, a service name that does not exist on the proxy's network, or a stale address after a deploy. Common the first time a proxy and an application are containerised together, because localhost in the proxy config means the proxy's container.

Confirm

sudo nginx -T 2>/dev/null | grep -A3 'proxy_pass\|upstream ' | head -30

The actual resolved configuration, not the file you think is loaded. nginx -T dumps every file nginx would load now, following all includes, which is often more than the file you edited. It reads from disk, so after an edit you still need nginx -s reload before the running nginx uses it.

Fix

  • Resolve the upstream name from inside the proxy: docker exec proxy getent hosts api. A name that does not resolve is a different fault from a connection refused.
  • In compose, the upstream is the service name and the container port. Neither localhost nor the published host port is correct from inside the proxy container.

The upstream is speaking a different protocol than the proxy expects

Occasional

The connection works and bytes come back, but they are not what the proxy is parsing. Usually proxy_pass http:// against a TLS listener, or an HTTP/1.1 proxy in front of a gRPC or HTTP/2-only service.

Confirm

curl -sv http://<upstream-host>:<port>/ -o /dev/null 2>&1 | head -20

A TLS handshake error, or a response that is not recognisable HTTP. If plain HTTP fails but https:// works, the scheme in proxy_pass is the fault.

Fix

  • Match the scheme to the listener: proxy_pass https:// for a TLS upstream, and grpc_pass for gRPC.
  • For an HTTP/2-only upstream, set proxy_http_version 2 (nginx 1.29.4 and later, built with ngx_http_v2_module) or put an HTTP/1.1-capable listener in front of it.

The upstream's response headers exceed the proxy's buffer

Occasional

The proxy allocates a fixed buffer for response headers. An unusually large set — a long chain of cookies, a large Set-Cookie, a verbose auth token — overflows it, and the proxy gives up with a 502 rather than truncating.

Confirm

sudo tail -50 /var/log/nginx/error.log | grep -i 'too big header\|upstream sent too big'

upstream sent too big header while reading response header from upstream.

Fix

  • Raise proxy_buffer_size and proxy_buffers to accommodate the real header size.
  • Then ask why the headers are that large. A multi-kilobyte cookie is usually a design problem that will cause other trouble.
proxy_buffer_size 16k;
proxy_buffers 4 16k;

Treat the raise as a stopgap and the header size as the actual finding.

Every upstream in the pool has been marked unavailable

Occasional

With multiple upstreams, the proxy takes failing ones out of rotation. Once all of them are out, every request 502s — including requests that would now succeed, because a passively-marked upstream is not retried until its window expires.

Confirm

sudo tail -50 /var/log/nginx/error.log | grep -i 'no live upstreams'

no live upstreams while connecting to upstream. That is the whole pool, not one member.

Fix

  • Fix the underlying upstream failure — the pool being empty is a consequence rather than the cause.
  • Review max_fails and fail_timeout. Aggressive values will eject the entire pool during a brief wobble and then refuse to try again for the timeout period.

Understanding it properly

Skip this if you are mid-incident — the working part of the page is above. Worth reading afterwards, because understanding the mechanism is what stops the next one.

In plain terms

Your request reached a proxy, such as nginx, whose job is to pass it on to the application behind it. A 502 means the proxy tried and did not get a valid answer: it could not connect, the application hung up, or what came back was not usable. The proxy writes down which of these happened in its error log, so that is where you start.

A refused upstream connection in nginx's error log, from nginx.org

Documented, not reproduced: nginx is not installed on this machine, and a stand-in proxy written in Python would not produce nginx's log lines. The entry is quoted from the error_log reference on nginx.org, which shows this failure in the JSON log format. That format needs nginx 1.29.8 or later and is part of the commercial subscription; open-source nginx records the same details on one plain-text line. The page does not show how the entry was read.

Command

sudo tail -n 1 <error-log-path>

Output · as shown in nginx — Core functionality: error_log

{
  "level": "error",
  "timestamp": "2026-05-13T10:30:15.042+00:00",
  "pid": 12345,  "tid": 12345,  "cnum": 3,
  "msg": "connect() failed",
  "client": "192.168.1.10",  "server": "example.com",
  "request": "GET /api HTTP/1.1",
  "upstream": "http://127.0.0.1:8080/api",
  "errno": 111,
  "errtext": "Connection refused"
}

How to read it

  • msg, errno and errtext are the failure: connect() failed, 111, Connection refused. nginx reached that address and nothing was accepting connections there, which is the not-listening cause.
  • upstream is where nginx actually tried: http://127.0.0.1:8080/api. Compare it with where the application listens. 127.0.0.1 is nginx's own machine or container, so an application in another container is never reached at that address.
  • client, server and request identify the request that failed, so you can match it with the access log and with the 502 a user reported.
  • Open-source nginx writes the same event as one text line: time, [error], process IDs, then connect() failed (111: Connection refused) while connecting to upstream, client: ..., server: ..., request: "...", upstream: "...". When the refusal comes back from the connect() call itself, the line starts connect() to <address> failed instead, so search for both.
  • If nginx had been waiting instead of being refused, the line would say upstream timed out and the client would get a 504, not a 502.

Words used on this page

Upstream
The server nginx forwards requests to, named in proxy_pass or in an upstream block.
Error log
nginx's log of problems, separate from the access log. Each upstream failure is one line naming what failed, the stage (while connecting to upstream), the client, the request and the upstream address.
Connection refused (111)
The answer when nothing accepts connections at that address and port. 111 is Linux's number for ECONNREFUSED; macOS uses 61, so the number in the log depends on the operating system.
Prematurely closed
nginx connected and sent the request, then the upstream closed the connection before sending a complete response header.
proxy_buffer_size
The buffer nginx reads the response header into, one memory page (4K or 8K) by default. A header bigger than the buffer makes the response invalid, and the client gets a 502.
max_fails and fail_timeout
Upstream settings that decide when nginx stops sending requests to a failing server and for how long. The defaults are 1 failure and 10 seconds. A group with a single server is never marked unavailable.
nginx -T
Tests the configuration and prints every configuration file nginx loads, includes and all. It reads from disk, so it shows what the next reload will use, not necessarily what is running.
What is actually happening

A 502 is a proxy telling you it tried and failed. That framing matters, because it means the proxy is working: it accepted your request, attempted to forward it, and is honestly reporting that the attempt did not produce a valid response.

So there are three places the fault can be, and they need different investigations. The proxy may be pointing at the wrong address. The network path may be refusing or resolving incorrectly. Or the upstream application may have accepted the connection and then died, closed early, or returned something that is not valid HTTP.

The proxy's own error log distinguishes these almost immediately, and it is the single most under-used artefact in this whole class of problem. connect() failed (111: Connection refused) and upstream prematurely closed connection are two completely different faults that produce the identical status code for the user.

It is worth being precise about one thing: a 502 is not a timeout. If the upstream took too long you get a 504. A 502 means the attempt ended in a way other than waiting.

How to tell this is your problem
WhereWhat you see
Browser / curlHTTP/1.1 502 Bad Gateway, often with the proxy's own branded error page.
nginx error logconnect() failed (111: Connection refused) while connecting to upstream, or upstream prematurely closed connection while reading response header.
Upstream application logsEither nothing at all (the request never arrived) or a crash at the moment of the request.

How to know it is actually fixed

  • A request through the proxy returns the application's own response rather than the proxy's error page.
  • The proxy error log stops adding entries for the affected upstream.
  • The 502 does not return under load, which distinguishes a fixed cause from one that only appears when workers are saturated.

Stopping it happening again

  • Alert on the proxy's upstream error rate rather than only on the application's own error rate. A 502 is the one failure the application may never see, because the request did not reach it.
  • Give the upstream a readiness signal the proxy respects, so a starting or draining instance is taken out of rotation rather than producing 502s.
  • Keep the proxy and the upstream's bind address in sync deliberately — this is the mismatch that recurs on every environment change.

Security considerations

  • Error log lines carry the client's IP address, the full request line including any query string, and internal upstream addresses. Redact them before you paste a log into a ticket or forum.
  • nginx -T prints every configuration file nginx loads, including anything written inline such as a proxy_set_header Authorization value. Filter it with grep, as the diagnose steps do, rather than sharing the whole dump.
  • Switching to proxy_pass https:// encrypts the hop but does not check who answers: proxy_ssl_verify is off by default. Turn it on and point proxy_ssl_trusted_certificate at the CA that signed the upstream's certificate.
  • When you fix a loopback bind by making the upstream listen on 0.0.0.0, do not also publish its port to the outside. Only the proxy needs to reach it, for example over a private Docker network.
  • nginx's built-in 502 page shows its version, because server_tokens is on by default. Set server_tokens off; so error pages do not advertise the exact release to clients.

Sources

Behaviour described here is drawn from official documentation. Where a figure could not be confirmed on an official page it is attributed in the text rather than stated as canonical.

Terms used here (2)

Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.

Ingress
A Kubernetes resource that manages external HTTP and HTTPS access to services within a cluster. An Ingress does nothing until an Ingress controller is installed; the controller handles TLS termination, name-based virtual hosting, and path-based routing. The Ingress API is stable but frozen — it is no longer being developed, though there are no plans to remove it — and Kubernetes recommends Gateway API for new work.
Nginx
A high-performance web server, reverse proxy, and load balancer. Nginx is widely used in DevOps for serving static content, terminating TLS, proxying requests to application servers, and acting as an Ingress controller in Kubernetes (through F5's NGINX Ingress Controller; the community Ingress NGINX project was retired in March 2026).

All 59 terms in the glossary

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Something wrong or out of date? Report it