BlueByte
502 Bad GatewayFixed

nginx: 502 Bad Gateway from an upstream

By Haneul SeoUpdated September 4, 20264 min

Hi, it's BlueByte. A 502 Bad Gateway from nginx means nginx itself is fine — it's the upstream behind it that didn't answer, or answered with something nginx couldn't use. The symptom is nginx returning 502 even though its own nginx -t passes. We'll walk through what a 502 really means, how to read the error log to find the exact failure, fix it per cause, and keep it from recurring.

What a 502 from nginx actually means

The browser shows a short page and the access log records the status:

203.0.113.7 - - [04/Sep/2026:11:20:03 +0900] "GET /api HTTP/1.1" 502 552 "-" "curl/8.4.0"

nginx accepted the request, tried to proxy it to the server named in proxy_pass, and either couldn't connect or got a reply it considers invalid. This is not nginx being down — nginx is up and answering; the gateway behind it is the problem. That's why restarting nginx rarely helps.

The four things that make an upstream unusable

A 502 comes from one of these:

  • The upstream isn't running or isn't listening on the address in proxy_pass — the connection is refused.
  • proxy_pass points at the wrong host, port, or socket — nginx connects to nothing.
  • The upstream crashed or closed the connection mid-response, so nginx read an empty or truncated reply.
  • The upstream's response header is larger than proxy_buffer_size (default 4k/8k), so nginx rejects it as invalid.

A slow-but-alive upstream is a separate error — that's a 504, covered at the end.

Read the error log — it names the exact failure

The access log shows 502; the error log says why. Watch it while you reproduce the request:

sudo tail -f /var/log/nginx/error.log
2026/09/04 11:20:03 [error] 812#812: *5 connect() failed (111: Connection refused)
  while connecting to upstream, client: 203.0.113.7, upstream: "http://127.0.0.1:8000/api"

Read the parenthetical. (111: Connection refused) means nothing is listening; upstream prematurely closed connection means the app died mid-reply; upstream sent too big header means the header overflowed the buffer; (13: Permission denied) usually means SELinux is blocking the outbound connection on RHEL. The upstream: field prints the exact address nginx tried — check it matches where your app actually listens.

Fix it by cause: dead upstream, wrong address, big headers

  1. Upstream not listening — confirm what's on the port, then start the app:
ss -ltnp | grep :8000
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/api

If curl also fails, nginx is innocent — fix the app. A working upstream prints 200.

  1. Wrong proxy_pass — point it at the real address and reload:
location /api {
    proxy_pass http://127.0.0.1:8000;
}
  1. Header too big — raise the buffers for that location:
proxy_buffer_size 16k;
proxy_buffers 8 16k;

A real case: the app crashed and left the socket dead

Your Node app behind nginx starts throwing 502s. nginx -t is fine, so you run tail -f /var/log/nginx/error.log and see connect() failed (111: Connection refused) while connecting to upstream: "http://127.0.0.1:3000/". You run ss -ltnp | grep :3000 and nothing is listening — the app process exited overnight and its port is free. You restart the service with systemctl restart myapp, curl http://127.0.0.1:3000/ returns 200, and the 502s stop immediately. nginx was reporting the truth all along: there was no gateway to reach.

Confirm the proxy path end to end

Test the config, reload, then request through nginx:

sudo nginx -t && sudo nginx -s reload
curl -I https://example.com/api
HTTP/1.1 200 OK

A 200 (or your app's normal status) through the public URL means nginx reached the upstream and passed the response back. Keep the error log open during the test — a clean request leaves no new [error] line.

Keep 502s from coming back

Run the upstream under a supervisor — systemd, a container restart policy, or a process manager — so a crash restarts it instead of leaving a dead port. Add a health check so an unready backend is pulled from rotation before it can serve a 502. On RHEL/SELinux hosts, allow nginx to make outbound connections once with sudo setsebool -P httpd_can_network_connect 1. Size proxy_buffer_size for the largest header your app sends — oversized auth tokens and cookies are a common overflow cause. If the 502s are intermittent and the log shows upstream prematurely closed connection, the backend is likely closing idle keepalive connections before nginx reuses them — set proxy_http_version 1.1 with an upstream keepalive count, or shorten the backend's idle timeout so nginx never picks a stale connection.

How a 502 differs from a 504

A 502 Bad Gateway means the upstream was unreachable or sent an invalid response — the connection failed or the reply was broken. A 504 Gateway Timeout means the upstream was reachable but too slow: it didn't answer within proxy_connect_timeout, proxy_send_timeout, or proxy_read_timeout (each default 60s). If the error log says upstream timed out, raise the relevant timeout or make the backend faster — that's a 504, not a 502.

Related questions

nginx -t passes but I still get a 502.

nginx -t only checks config syntax, not whether the upstream is alive. A valid config pointing at a dead backend still returns 502. Read the error log and check the upstream port with ss -ltnp.

The error log says 'upstream prematurely closed connection'.

The backend accepted the connection then died or reset before finishing the response — often an app crash or an out-of-memory kill. Check the app's own logs and restart it under a supervisor.

I get 'upstream sent too big header'.

The response header exceeded proxy_buffer_size. Raise proxy_buffer_size and proxy_buffers for that location; large auth cookies or tokens are the usual cause.

It's 502 on RHEL even though the app is up and curl works locally.

SELinux is likely blocking nginx from making the outbound connection — look for '(13: Permission denied)' in the error log. Allow it with sudo setsebool -P httpd_can_network_connect 1.

Is a 502 the same as a 504?

No. A 502 means the upstream was unreachable or replied with something invalid; a 504 means it was reachable but didn't answer within the proxy timeouts (default 60s). The error log line tells them apart.

References

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

More in this category

detected dubious ownershipFixed

Git: fatal: detected dubious ownership in repository

Since the CVE-2022-24765 fix in Git 2.35.2, Git refuses to read a repository whose working tree or .git directory is owned by a different user than the one running the command. It shows up in containers, CI jobs, sudo sessions and shared drives. Fix the ownership if the repo should be yours, or add the exact path to safe.directory in your global config — never in the repo's own config, which Git ignores for this.

Git
failed calling webhookFixed

Kubernetes: Internal error occurred: failed calling webhook

An admission webhook sits in front of your write, the API server could not get an answer out of it, and failurePolicy: Fail turned that silence into a rejection. The tail of the message is the whole diagnosis: context deadline exceeded means the call went nowhere, no endpoints available means nothing is running, and an x509 line means the API server does not trust the webhook's certificate. Each has a different fix, and none of them is your manifest.

Kubernetes
1205Fixed

MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

A statement waited the full innodb_lock_wait_timeout for a row lock another transaction is still holding, and gave up. sys.innodb_lock_waits names the blocking session and hands you the KILL statement, and a blocking_query of NULL means the blocker is idle on an open transaction. The detail most retry loops get wrong: by default only the timed-out statement is rolled back, so your transaction is still open and still holds every lock it took earlier.

MySQL
MISCONFFixed

Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

Reads keep working and every write is rejected, because the last background save failed and stop-writes-on-bgsave-error defaults to yes. The log names the real cause — no space, a dir the redis user can't write, a read-only mount at rename time, or fork failing with Cannot allocate memory. Fix the cause, run one BGSAVE, and writes come back on their own with no restart: rdb_last_bgsave_status flips from err to ok. Setting stop-writes-on-bgsave-error no restores writes instantly but leaves the snapshot broken, so treat it as a deliberate trade, not the fix.

Redis
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryFixed

Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (exit 134)

The V8 heap has its own ceiling, derived from system memory and the Node release, and it is often far below the RAM you have; when a build or server reaches it, V8 aborts with FATAL ERROR: Reached heap limit and exit code 134. Read the real limit with v8.getHeapStatistics().heap_size_limit, then raise it with --max-old-space-size (in MiB) or NODE_OPTIONS for a large workload, size it below the cgroup limit inside containers, and use --heapsnapshot-near-heap-limit to catch a leak in a long-running process. Exit 137 with no FATAL ERROR line is a container kill, not this.

Node.js
exec /docker-entrypoint.sh: exec format errorFixed

Docker: "exec format error" when the container starts — wrong-platform image, no emulator, or a script with no shebang

The container exits on its first instruction with exec format error — the kernel's ENOEXEC, meaning the file exists but cannot be executed here. In practice that is an image built on one CPU architecture (an Apple-silicon Mac produces linux/arm64) and run on another (an x86_64 server) with no QEMU handler registered in binfmt_misc, or an entrypoint script whose first line is not a shebang. uname -m, docker image inspect and ls /proc/sys/fs/binfmt_misc tell the causes apart; the fix is an explicit docker buildx build --platform (or a manifest list for both), QEMU registration or --platform when you mean to emulate, and a #!/bin/sh line for the script.

Docker