# 502 Bad Gateway in Nginx After Deploy: Read the Error Log

> 502 Bad Gateway means nginx got no valid response from your app. Match the error.log line (111 refused, 104 reset, closed early) to the cause and fix.

1. [Home](/)
2. [Blog](/blog)
3. 502 Bad Gateway in Nginx After Deploy: Read the Error Log

DeploymentOctober 9, 20269 min read

# 502 Bad Gateway in Nginx After Deploy: Read the Error Log

A 502 Bad Gateway page from nginx says nothing about the cause. The matching line in error.log does: connection refused, a worker that died mid-response, a keep-alive reset or an oversized header. What each one means and how to fix it after a deploy.

## The short version

A 502 Bad Gateway from nginx means nginx reached for your application and got either no response or an unusable one. The error page never says which. The line nginx wrote to error.log at the same moment does: connect() failed (111: Connection refused) means nothing was listening, upstream prematurely closed connection means the app died or hung up mid-request, recv() failed (104: Connection reset by peer) often means a keep-alive timeout mismatch or an app restart, and upstream sent too big header means the response headers outgrew the proxy buffer. After a deploy, the usual cause is the first one: the app listens on the wrong port or on 127.0.0.1, or traffic arrived before it was listening.

[Teo Marquardt](/about#author)· Facts last checked October 9, 2026

You deployed, opened the site, and got a white page with two lines on it:

html

```
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx</center>
</body>
</html>
```

The page is generated by nginx, not by your application, and it is identical whatever went wrong. That's why most advice for this error is a list of guesses, usually starting with "restart it" and ending with "raise the timeout". You don't have to guess. Every time nginx returns a 502, it writes one line to its error log saying why, and there are only a handful of possible lines. Read that line first and the list of suspects drops to one. Below are the lines you will actually see, what each means, the extra causes that show up right after a deploy, and how to tell 502 apart from 503 and 504.

## What 502 Bad Gateway means

A request to a site behind nginx travels in two hops: browser to nginx, then nginx to your application, which nginx calls the upstream. [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-502-bad-gateway) defines 502 as a gateway or proxy that "received an invalid response from an inbound server". In practice nginx also uses it when there was no response at all: the connection was refused, or it was dropped before a complete response header arrived. Either way, the first hop worked. nginx is up and answering. The second hop failed, and nearly always the cause is on your application's side of it.

### 502 vs 503 vs 504

The three 5xx codes a proxy returns are easy to mix up, and the difference narrows the search before you open any log:

| Code                    | What the proxy is telling you                                | Typical cause                                                                                                                   |
| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| 502 Bad Gateway         | I reached for the upstream and got nothing usable            | App not listening, crashed mid-request, connection reset, malformed or oversized response headers                               |
| 503 Service Unavailable | I won't or can't pass this on right now                      | Maintenance mode, overload, or a rate or connection limit at the proxy (nginx limit\_req and limit\_conn return 503 by default) |
| 504 Gateway Timeout     | The upstream accepted the request and never answered in time | A slow query or blocked event loop that outlasts proxy\_read\_timeout (60 seconds by default)                                   |

A 504 is a slow app; a 502 is an absent or broken one. That one distinction already rules out the timeout settings that half the advice on 502 tells you to raise. Raising `proxy_read_timeout` fixes 504s, and does nothing for a 502.

## Find the line in error.log first

On a server with a distribution package, the log is at `/var/log/nginx/error.log`. With the official nginx Docker image it goes to the container's standard error, so `docker logs <nginx-container>` shows it. Find the entry with the same timestamp as your failed request. It looks like this:

text

```
2026/10/09 09:14:02 [error] 31#31: *4521 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.7, server: app.example.eu, request: "GET / HTTP/1.1", upstream: "http://10.0.1.12:3000/", host: "app.example.eu"
```

Two parts matter. The phrase in the middle is the reason, and the `upstream:` field is the exact address and port nginx tried. Here is what each reason means:

| Line in error.log                                                                         | What happened                                                                                                                                        | Where to look                                                |
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| connect() failed (111: Connection refused) while connecting to upstream                   | Nothing was listening at the upstream address                                                                                                        | Is the app running? Right port? Listening on 0.0.0.0?        |
| upstream prematurely closed connection while reading response header from upstream        | The app accepted the request, then closed the connection without answering                                                                           | App logs at the same second: crash, OOM kill, worker timeout |
| recv() failed (104: Connection reset by peer) while reading response header from upstream | The app reset the connection                                                                                                                         | Keep-alive timeouts between nginx and the app; app restarts  |
| upstream sent too big header while reading response header from upstream                  | The response headers did not fit in nginx's buffer                                                                                                   | Large cookies or headers; proxy\_buffer\_size                |
| no live upstreams while connecting to upstream                                            | Every server in a multi-server upstream group is marked unavailable, after max\_fails failures (default 1) within fail\_timeout (default 10 seconds) | The errors that came before this one in the log              |

Log first, then theories

If you can't see the nginx error log, the most useful thing to do is get access to it. Every fix below depends on knowing which of these lines you have, and they have nothing in common except the status code.

## connect() failed (111: Connection refused) while connecting to upstream

Errno 111 is `ECONNREFUSED`: nginx reached the address in the `upstream:` field and the operating system there replied that nothing listens on that port. It's the same refusal your own code sees when [your app can't reach its database](/blog/econnrefused-127-0-0-1), only now your app is the one not answering. Four causes cover almost every case.

### The app is not running

It crashed at startup, it's in a restart loop, or it never started because the build output is missing. Check the application's own log and its process or container status. A crash loop produces 502s in bursts that line up with each restart.

### The app listens on a different port

nginx is pointed at 3000 and the app started on 8080, or the app reads `PORT` from the environment and the variable holds a different value than the proxy config expects. Compare the port in the `upstream:` field with the port the app reports at startup. If the app prints nothing at startup, make it print its address. One log line, and next time this check takes five seconds.

### The app listens on 127.0.0.1 inside its container

This is the one that fools people, because the app is running and the port is right. Many development servers bind to loopback by default: `flask run`, Django's `runserver`, `rails server` in development and Vite's dev server all listen on loopback (`127.0.0.1`, or `::1` in some Vite and Node setups) unless told otherwise. Inside a container or on a separate VM, loopback is reachable only from that same machine, so a proxy anywhere else gets refused. Check from inside the app's environment:

bash

```
# What is listening, and on which address?
ss -ltnp
# LISTEN 0  511  127.0.0.1:3000  0.0.0.0:*   <- only reachable from inside
# LISTEN 0  511    0.0.0.0:3000  0.0.0.0:*   <- reachable by the proxy

# Does the app answer locally at all?
curl -i http://127.0.0.1:3000/
```

If `curl` works from inside and nginx still gets refused, the bind address is the problem. Start the server on `0.0.0.0` (for example `flask run --host=0.0.0.0` or `gunicorn --bind 0.0.0.0:8000`) and run a production server rather than the development one while you're at it.

### proxy\_pass points at localhost inside a container

The mirror image of the previous cause. When nginx itself runs in a container, `localhost` in its config means the nginx container, not the app container next to it. In Docker Compose, use the service name:

nginx

```
location / {
    # proxy_pass http://localhost:3000;   # 502: localhost is the nginx container
    proxy_pass http://app:3000;           # "app" is the Compose service name
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}
```

On a PHP stack the same failure names a socket instead of a port: `connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)` means PHP-FPM is not running or uses a different socket path, and `(13: Permission denied)` means nginx's user can't open the socket file.

## upstream prematurely closed connection while reading response header from upstream

This time the connection worked. nginx sent the request, the app accepted it, and then the connection closed before a response header arrived. Something on the app side ended the request without answering. Look at the application log at the same second, and you'll usually find one of these:

* The process was killed. A container that hits its memory limit is killed instantly, mid-request, and the requests it was serving end exactly like this. If the container's exit code is 137, [the kernel or the orchestrator sent SIGKILL](/blog/exit-code-137).
* A worker timed out. gunicorn kills a sync worker that takes longer than `--timeout`, which is 30 seconds by default, and the request it was handling gets no response. nginx would have waited 60 seconds, so the 502 arrives before nginx's own timeout and looks like a crash.
* An unhandled exception took the process down. In Node, an uncaught error in a request handler can exit the whole process, and every request in flight gets the same 502.
* The app closed an idle keep-alive connection just as nginx reused it. It's the same race as in the next section: a clean close produces this line, a reset produces the 104 one.

The fix depends on which one it was. An OOM kill needs more memory or less of it per request. A worker timeout needs a longer `--timeout`, or better, a background job for the slow work. A crash needs an error handler that returns a 500 instead of taking the process down with it.

## recv() failed (104: Connection reset by peer)

Errno 104 is `ECONNRESET`. The full line usually ends with `while reading response header from upstream`. Apart from an app that restarts, a common cause is a keep-alive timeout mismatch. nginx does not reuse upstream connections by default. It starts once you set `keepalive` in an `upstream` block together with `proxy_http_version 1.1;` and `proxy_set_header Connection "";`, and many load balancers pool connections on their own. In that setup each side has its own idle timeout. If the app closes an idle connection at the same moment the proxy sends a new request down it, the request lands on a closed socket and fails. The result is rare, random 502s under light load that no one can reproduce.

Node.js makes this easy to hit: as of writing, `server.keepAliveTimeout` defaults to 5 seconds, while nginx, once upstream keepalive is on, keeps idle connections for 60 seconds by default (`keepalive_timeout` in the upstream module) and many cloud load balancers use 60 seconds or more. The rule is that the app should hold an idle connection longer than the proxy does, so the proxy is always the side that closes it:

javascript

```
const server = app.listen(port, "0.0.0.0");

// Keep idle connections open longer than the proxy in front does,
// so the proxy always closes first and never reuses a dead socket.
server.keepAliveTimeout = 65_000;
// Must be larger than keepAliveTimeout.
server.headersTimeout = 66_000;
```

Other servers have the same setting under another name, for example gunicorn's `--keep-alive` or uvicorn's `--timeout-keep-alive`. The values matter less than the order: the app's idle timeout must be longer than the proxy's.

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

nginx reads the response headers into a buffer whose size is `proxy_buffer_size`, one memory page by default (4 KB or 8 KB depending on the platform). A response with large `Set-Cookie` headers, for example a session stored in a cookie or an authentication library that sets several large cookies at once, can overflow it. The request works for some users and fails with 502 for others, usually the ones with the most cookies. Shrinking the headers is the proper fix. Raising the buffer gets you through the afternoon:

nginx

```
proxy_buffer_size 16k;
proxy_buffers 8 16k;
```

This is the 5xx cousin of [413 Request Entity Too Large](/blog/413-request-entity-too-large): there nginx rejects a request body that is too big, here it rejects response headers that are too big.

## 502 right after a deploy

If the 502s started with a release, the log line will usually be the refused connection, and the cause is one of a short list:

* The port moved. The new build listens on a different port than the old one, or the code ignores the port the platform tells it to use (often through a `PORT` variable) in favour of a hardcoded number.
* The new build binds 127.0.0.1\. A changed start command, a framework upgrade, or a switch to the development server.
* Traffic arrived before the app was listening. The proxy sent requests to the new instance during its startup, while it was still running migrations, warming a cache or connecting to the database. This lasts seconds and then fixes itself, which is why it's easy to dismiss.
* The old instance was stopped with requests in flight. If the old process exits on SIGTERM without finishing what it was serving, those requests end as `upstream prematurely closed connection`. A [rolling or blue-green deploy](/blog/blue-green-vs-rolling-deployments) only helps if the old instance finishes its in-flight work before it exits.

The first two are configuration and stay broken until fixed. The last two are timing, and the fix is a health check the platform waits on before sending traffic, plus a graceful shutdown. A minimal server that gets all four right:

javascript

```
// Use the port from your service settings; PORT is a common convention.
const port = Number(process.env.PORT ?? 3000);

// Healthy only when the app can actually serve: it touches the database.
app.get("/healthz", async (req, res) => {
  try {
    await pool.query("SELECT 1");
    res.sendStatus(200);
  } catch {
    res.sendStatus(503);
  }
});

const server = app.listen(port, "0.0.0.0", () => {
  console.log(`listening on 0.0.0.0:${port}`);
});

// On SIGTERM stop accepting new connections, finish the ones in flight, then exit.
process.on("SIGTERM", () => {
  server.close(() => process.exit(0));
  server.closeIdleConnections?.(); // Node 18.2+: drop idle keep-alive sockets too
});
```

Note what the health check does. An endpoint that returns 200 without touching anything only proves the port is open, so a release that can't reach its database passes the check, takes traffic and fails every request. A check that runs `SELECT 1` keeps that release out of rotation.

## How Runsite handles it

[Runsite web services deploy with rolling releases gated by a health check](/services/web-services). New instances start alongside the ones already serving, traffic moves to them only after the health check passes, and a release that fails the check is rolled back automatically, so the old version keeps answering. That keeps the "traffic arrived before the app was listening" case away from users, as long as the health check proves the app can actually serve. The health check path and interval are configurable, which is how you point it at an endpoint like the `/healthz` above. Containers that fail are restarted and rescheduled onto healthy nodes, and the services run on servers in Germany.

What the platform can't fix for you is the app itself: it still has to listen on `0.0.0.0` and on the port the platform sends traffic to, and handle SIGTERM by finishing its in-flight requests. Service settings are described in the [Runsite docs](https://docs.runsite.app).

## The short version

* A 502 means nginx reached for your app and got no usable response. The error page is the same every time; the line in error.log is not.
* `connect() failed (111: Connection refused)`: nothing listens at the `upstream:` address. Check that the app runs, the port matches, and it binds 0.0.0.0, not 127.0.0.1.
* `upstream prematurely closed connection`: the app died or hung up mid-request. Look for an OOM kill, a worker timeout or an uncaught exception at the same second.
* `recv() failed (104: Connection reset by peer)`: often a keep-alive mismatch or a restarting app. If the proxy reuses connections, make the app's idle timeout longer than the proxy's; otherwise look for a restarting app.
* 502s right after a deploy are a wrong port, a loopback bind, or traffic reaching an instance before it's ready. A health check that touches the database plus a graceful shutdown fixes the timing cases.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](/services/web-services)

[Back to all articles](/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does the 502 Bad Gateway error mean in Nginx?

It means nginx, acting as a reverse proxy, tried to pass the request to your application (the upstream) and got no valid response: the connection was refused, closed or reset before a complete response header arrived, or the headers were too large for nginx's buffer. nginx itself is working. The exact reason is in the nginx error log, in a line such as connect() failed (111: Connection refused) while connecting to upstream.

### Is a 502 Bad Gateway my fault?

If you are visiting someone else's site, no. The problem is between that site's proxy and its application, and the only thing to do is wait or try again later. If it is your site, the cause is almost always your application or its configuration rather than nginx: the app is not running, listens on the wrong port or address, crashed during the request, or closes keep-alive connections earlier than the proxy expects.

### How to fix 502 Bad Gateway on nginx?

Open the nginx error log (/var/log/nginx/error.log, or docker logs for the nginx container) and find the line logged with the failed request. For connect() failed (111: Connection refused), make sure the app is running, listens on the port in the upstream address, and binds 0.0.0.0\. For upstream prematurely closed connection, check the app log for crashes, out-of-memory kills or worker timeouts. For recv() failed (104: Connection reset by peer), raise the app's keep-alive timeout above the proxy's. For upstream sent too big header, reduce cookie size or raise proxy\_buffer\_size.

### What is the difference between 502 and 504?

A 502 Bad Gateway means the proxy got no usable response from the upstream: nothing was listening, or the connection broke before the response header arrived. A 504 Gateway Timeout means the upstream accepted the request but did not respond within the proxy's timeout, which in nginx is usually proxy\_read\_timeout (60 seconds by default); proxy\_connect\_timeout, also 60 seconds, produces a 504 when the upstream address does not answer at all. A 502 points at an absent or crashing app; a 504 points at a slow one, and only the 504 is fixed by changing timeouts.

Keep reading

## Related articles

[Deployment9 min readECONNREFUSED 127.0.0.1: Your App Is Calling ItselfError: connect ECONNREFUSED 127.0.0.1:6379 after a deploy means your app is calling its own machine. Why the address says localhost, the ::1 variant, and how to fix each cause for Redis and Postgres.Oct 7, 2026Read](/blog/econnrefused-127-0-0-1)[Deployment10 min readExit Code 137: SIGKILL, Not Always Out of MemoryExit code 137 says a process was killed with SIGKILL. It doesn't say who sent it. How to tell a memory kill from a missed SIGTERM on deploy or a failing probe, and what fixes each one.Oct 5, 2026Read](/blog/exit-code-137)[Deployment11 min readBlue-Green vs Rolling Deployments: What Zero Downtime Costs YouBoth strategies promise your users never see the release. The difference that decides between them is how many versions of your code are talking to one database at the same time.Aug 17, 2026Read](/blog/blue-green-vs-rolling-deployments)

## Your app deserves to be online

€5 of credit on signup. Deploy in under a minute. No credit card needed.

[Start deploying](https://dashboard.runsite.app/login)[View documentation](https://docs.runsite.app)

---

Source: https://runsite.app/blog/502-bad-gateway-nginx
