Skip to main content

Guides / HTTP status codes explained

502 Bad Gateway: What It Means and How to Fix It

A 502 Bad Gateway means a proxy, load balancer or CDN in front of the site asked the server behind it for a response and got an invalid one, or none at all. The component that shows the error is working; the fault is in the backend behind it or in the connection between the two.

What a 502 tells you

RFC 9110 defines 502 for a server acting as a gateway or proxy that received an invalid response from an inbound server it accessed while trying to fulfill the request. Most sites have at least one such hop: nginx or Apache in front of an application server, a load balancer in front of several servers, a CDN in front of the origin, or all three.

So a 502 splits your stack in two: whatever produced the error page is healthy enough to answer, and something behind it is not. Two details narrow it down. The page often says who generated it: nginx prints its name at the bottom, Apache titles the page "Proxy Error", and CDNs and cloud load balancers use their own branded pages. And a fast 502 most likely means a refused connection; one that takes 30 or 60 seconds, a backend that hung until a timeout cut the request off.

502 vs 503, 504 and Cloudflare 520-522

All of these codes come from the layer between the visitor and the application, and the 5xx overview covers the whole class. What separates them is what happened on the connection to the backend.

ErrorWhat happened on the backend connectionWhere to look first
502 Bad GatewayRefused connection, dropped connection, or a response the proxy could not parseProxy error log, then whether the backend process runs
503 Service UnavailableA server declined the request: overload, maintenance, or no healthy backend to send it toMaintenance flags, capacity, health checks
504 Gateway TimeoutThe backend accepted the request but did not answer within the proxy's timeoutSlow queries and slow dependencies
Cloudflare 520The origin's answer was empty, malformed or cut offOrigin crash logs, response header size
Cloudflare 521The origin refused Cloudflare's TCP connectionWhether the web server runs, firewall rules for Cloudflare's IP ranges
Cloudflare 522Cloudflare's TCP connection to the origin timed outFirewall drops, server load, the origin IP in DNS

The mapping is not universal, because each proxy chooses its own code for the same fault:

  • nginx returns 502 when it cannot connect or the connection breaks, and 504 when its connect or read timeout runs out.
  • Apache mod_proxy_http returns 503 when it cannot connect, and 502 when the backend hangs up or ProxyTimeout expires before a status line arrives.
  • Envoy and gateways built on it return 503 for refused and reset connections ("upstream connect error or disconnect/reset before headers") and 502 for protocol errors.
  • ingress-nginx in Kubernetes returns 503 when a Service has no ready endpoints.
  • Cloudflare turns its own failed connections to the origin into 52x codes, and shows a Cloudflare-branded 502 when your origin returned one.

Common causes

  • The backend is not running: a crashed app server, a stopped PHP-FPM pool, an exited container.
  • The backend died mid-request: an out-of-memory kill, a segmentation fault, or a worker killed by its own timeout.
  • Wrong address, port or socket path, often left over from a migration, a PHP upgrade or a container rebuild.
  • The backend listens on 127.0.0.1, so another container, pod or host cannot reach it.
  • Unix socket permissions: the web server's user cannot open the PHP-FPM or app socket.
  • Scheme mismatch: TLS sent to a plain HTTP port, or plain HTTP to a TLS-only one.
  • Response headers too large for the proxy's header buffer, such as an oversized cookie.
  • Keep-alive races: the proxy reuses an idle connection at the moment the backend closes it.
  • Restarts and empty pools: the gap during a deploy, or an upstream group whose servers are all marked failed.

Reading the proxy error log

The status code is generic, but the line the proxy writes to its error log at the same second is specific. Linux packages of nginx write it to /var/log/nginx/error.log:

[error] connect() failed (111: Connection refused) while connecting to upstream,
        upstream: "http://127.0.0.1:8080/"

[error] upstream prematurely closed connection while reading response header
        from upstream

[error] recv() failed (104: Connection reset by peer) while reading response
        header from upstream

[error] upstream sent too big header while reading response header from upstream

[error] no live upstreams while connecting to upstream
  • Connection refused: nothing listens on that address and port. The backend is stopped, still starting, bound to another interface, or on another port.
  • upstream prematurely closed connection: the backend took the request and hung up before sending headers. Look for a crash, a memory kill or a worker timeout in the backend's own log. From PHP-FPM it reads upstream prematurely closed FastCGI stdout.
  • Connection reset by peer: the same, but reset instead of closed. Typical of a killed worker, a firewall in between, or a pooled connection the backend had already closed.
  • upstream sent too big header: a buffer limit in nginx, not a backend fault (see the buffer settings below).
  • no live upstreams: every server in an upstream group is marked failed. By default one failure takes a server out for 10 seconds (max_fails=1, fail_timeout=10s), so scroll up for the error that tripped it.

On Apache, a refused connection gives a 503; a backend that hangs past ProxyTimeout or closes without answering gives the 502:

[proxy:error] (111)Connection refused: AH00957: HTTP: attempt to connect to
    127.0.0.1:8080 (127.0.0.1:8080) failed
[proxy:error] AH00959: ap_proxy_connect_backend disabling worker for
    (127.0.0.1:8080) for 60s
[proxy_http:error] AH01114: HTTP: failed to make connection to backend: 127.0.0.1

[proxy_http:error] (70007)The timeout specified has expired: AH01102: error
    reading status line from remote server 127.0.0.1:8080
[proxy:error] AH00898: Error reading from remote server returned by /report

The "disabling worker" line explains why Apache can keep failing after the backend recovers: it stops sending requests to that backend for the retry period, 60 seconds by default, and answers 503 meanwhile. retry=0 on the ProxyPass line removes the delay.

How to fix it

  1. Test the backend directly from the proxy host:
    curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/
    A 200 here with a 502 on the public URL puts the fault in the proxy config or the path between the two. A refused connection means the backend is down.
  2. Check what is listening where the proxy dials: sudo ss -ltnp for TCP ports, sudo ss -xlp for Unix sockets.
  3. Match the proxy log line at the time of the 502 against the lists above before changing anything. On managed hosting, find it in the control panel or ask the host.
  4. Read the backend log for the same second. A crash, a worker timeout or an out-of-memory kill (journalctl -k | grep -i "out of memory") confirms the "prematurely closed" case.
  5. Check the upstream address, port, socket path and scheme. sudo nginx -T prints the whole loaded configuration.
  6. Apply the fix for your stack from the sections below, then sudo nginx -t && sudo systemctl reload nginx.
  7. Make deploys overlap: start the new process, wait for its health check, then drain and stop the old one.

PHP-FPM

On PHP sites, nginx names the PHP-FPM problem directly:

[crit] connect() to unix:/run/php/php8.2-fpm.sock failed (2: No such file or
       directory) while connecting to upstream
[crit] connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied)
       while connecting to upstream
[error] connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource
        temporarily unavailable) while connecting to upstream
  • No such file or directory: PHP-FPM is stopped, or fastcgi_pass names the wrong socket. Debian and Ubuntu name the socket after the PHP version, so an upgrade from 8.1 to 8.2 moves it.
  • Permission denied: the user nginx runs as (www-data on Debian and Ubuntu, nginx in the nginx.org and RHEL packages) needs read and write access to the socket, set by listen.owner, listen.group and listen.mode.
  • Resource temporarily unavailable: every child is busy and the socket's queue is full. The PHP-FPM log shows server reached pm.max_children setting (5), consider raising it; 5 is the value in the sample pool file PHP ships. Requests that wait in the queue instead end as 504s once they pass fastcgi_read_timeout.
; /etc/php/8.2/fpm/pool.d/www.conf (Debian and Ubuntu layout)
listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
request_terminate_timeout = 60s

Size pm.max_children from memory: divide the RAM you can give PHP by the resident memory of one busy child, as top shows it. Going higher trades 502s for out-of-memory kills. request_terminate_timeout kills a child that runs too long and logs execution timed out (...), terminating; nginx sees the connection drop and returns 502, so a slow script can surface as a 502 rather than a 504. With Apache in front, these socket failures appear as AH02454 and AH01079, with a 503.

Node.js, Gunicorn and other app servers

An app server that crashes leaves nothing listening until systemd, pm2 or a Docker restart policy brings it back, and the proxy logs connection refused for that whole window. journalctl -u myapp --since "15 min ago" shows the exit reason; in Node.js, an uncaught exception ends the process. Three defaults cause 502s in otherwise healthy apps:

  • Bind address. Gunicorn binds to 127.0.0.1:8000 unless --bind or a PORT variable says otherwise, and another container cannot reach that address. Use gunicorn --bind 0.0.0.0:8000 app:app in a container; the same goes for a Node app started with app.listen(3000, "127.0.0.1").
  • Worker timeout. With the default sync workers, Gunicorn kills a worker silent for longer than --timeout (30 seconds by default) and logs [CRITICAL] WORKER TIMEOUT (pid:4242). The proxy sees the connection drop, so a slow Django or Flask view shows up as a 502 at the 30-second mark.
  • Keep-alive timeout. In current Node.js release lines, server.keepAliveTimeout defaults to 5 seconds, while an AWS Application Load Balancer keeps idle connections for 60. The balancer can send a request down a connection Node is closing, and that request gets a 502; AWS recommends an application idle timeout longer than the balancer's.
const server = app.listen(3000, "0.0.0.0");
server.keepAliveTimeout = 65000; // longer than the balancer's 60-second idle timeout

Docker

  • Container port, not host port. With -p 8080:3000, another container on the same network reaches the app at app:3000; app:8080 is refused.
  • Listen on all interfaces. A process bound to 127.0.0.1 inside the container is unreachable from other containers and through published ports.
  • Restarts and memory kills. docker inspect app | grep -E '"(Status|RestartCount|OOMKilled)"' shows a crash loop or a memory kill.
  • Stale addresses. nginx resolves a proxy_pass hostname once, at startup, so after the app container is recreated with a new IP, nginx keeps dialing the old one. Reload nginx, or resolve at request time through Docker's embedded DNS:
resolver 127.0.0.11 valid=10s;

location / {
    set $app http://app:3000;
    proxy_pass $app;
}

Load balancers and Kubernetes ingress

AWS's list of 502 causes for its Application Load Balancer matches a proxy's: the target refused or reset the connection, sent a malformed response or oversized headers, closed a keep-alive connection while a request was outstanding, or failed the TLS handshake. With no registered targets, it answers 503 instead.

A Kubernetes ingress controller proxies straight to the pod addresses in the Service's EndpointSlices. With no ready endpoints (a selector that matches no pods, or every pod failing its readiness probe), ingress-nginx answers 503. A 502 means it had addresses and the connection failed: a targetPort that differs from the port the container listens on, an app bound to 127.0.0.1, or pods stopped mid-request during a rollout. Start with kubectl get endpointslices -l kubernetes.io/service-name=web and kubectl describe pods -l app=web.

Kubernetes removes a terminating pod from the EndpointSlices at the same time as it starts shutting it down, so the ingress can still send it requests for a moment. A short preStop sleep keeps the pod serving until the ingress catches up; it counts against terminationGracePeriodSeconds, 30 seconds by default.

# Service
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080   # the port the app really listens on
---
# Deployment pod template
containers:
  - name: web
    ports:
      - containerPort: 8080
    readinessProbe:
      httpGet:
        path: /healthz
        port: 8080
      periodSeconds: 5
    lifecycle:
      preStop:
        exec:
          command: ["sleep", "10"]

Timeouts, keep-alive and buffer settings

Whether a slow backend ends as a 502 or a 504 depends on which timeout fires first. If the backend's limit runs out first (Gunicorn's --timeout, PHP-FPM's request_terminate_timeout), it drops the connection and nginx returns 502. If nginx's proxy_read_timeout or fastcgi_read_timeout runs out first (60 seconds by default), nginx returns 504 and logs upstream timed out (110: Connection timed out). Apache's mod_proxy_http returns 502 either way.

upstream app {
    server 127.0.0.1:3000;
    keepalive 16;
    keepalive_timeout 4s;    # shorter than the backend's own idle timeout
}

server {
    location / {
        proxy_pass http://app;
        proxy_connect_timeout 5s;
        proxy_read_timeout 120s;
        proxy_buffer_size 16k;
        proxy_buffers 8 16k;
        proxy_busy_buffers_size 32k;
    }
}
  • proxy_connect_timeout covers the TCP connect only (60 seconds by default); a short value fails fast on an unreachable host.
  • proxy_read_timeout is the longest gap between two reads from the backend, not a limit on the whole response.
  • proxy_buffer_size holds the first part of the response, including the headers, and defaults to one memory page (4k or 8k). Raise proxy_buffers with it: on a 4k-page system, raising proxy_buffer_size alone to 16k makes nginx -t fail with a proxy_busy_buffers_size error. Then find out why the headers grew.
  • keepalive reuses backend connections. Since nginx 1.29.7 it is on by default (32 idle connections per worker, HTTP/1.1); older versions also need proxy_http_version 1.1 and an empty Connection header. Keep keepalive_timeout (60 seconds by default) below the backend's idle timeout, so nginx closes idle connections first.

nginx retries a request that failed on a reused connection, but not a POST or PATCH it has already sent, so keep-alive races show up as occasional 502s on form submissions and API writes. For PHP-FPM the same settings carry a fastcgi_ prefix.

What a check from several locations shows

A curl from your machine tells you the site returned 502 once. The same request from several regions at once tells you which layer to suspect:

  • Every location gets a fast 502: the proxy is up and the backend refuses connections.
  • Every location gets a 502 after the same long wait, such as 30 or 60 seconds: a timeout cut the request off. Compare the delay with Gunicorn's, PHP-FPM's or Apache's limit.
  • Some checks fail and some pass, in every region alike: one bad server in a pool, PHP-FPM at pm.max_children, or keep-alive races.
  • Only locations behind one region or load balancer fail: one deployment or edge-to-origin path is broken while the rest works.

The HTTP check sends the request from 300+ locations and reports the status code, response time and error each location saw.

Frequently asked questions

Is a 502 my fault or the website's?

As a visitor, almost always the website's. The error comes from a server on their side, so clearing your cache or changing DNS will not fix it, and a 502 caused by a restart clears by itself once the backend is back. The exception is a proxy or VPN on your side, which is also a gateway: if every site shows a 502, check that first.

Do 502 errors hurt SEO?

Short ones do not. Google's crawlers slow down while a site returns 5xx errors and speed up again once it returns 2xx, and indexed URLs are kept. URLs that keep returning server errors are eventually dropped, so a 502 that lasts for days becomes a search problem as well.

Why does Cloudflare show a 502 when my server seems fine?

According to Cloudflare's documentation, a Cloudflare-branded 502 page means your origin returned the 502, and a blank page without branding means Cloudflare produced it. Test the origin with the edge out of the way: curl -skI --resolve example.com:443:203.0.113.10 https://example.com/ with your origin IP (-k because a Cloudflare origin certificate fails public checks). For 520, 521 or 522, use the guides in the table above.

Why do 502s come and go?

Intermittent 502s usually come from one unhealthy server in a pool, a worker pool that fills up at peak traffic, keep-alive races, or the restart window of each deploy. Line up the error times with deploys, traffic peaks and the backend's warnings.

Catching a 502 that lasts twenty seconds

Deploy-time and crash-loop 502s are short: a backend restarts, errors for twenty seconds and recovers, and by the time anyone looks there is nothing left to see. A check that runs on a schedule from several locations records each of those windows with a timestamp, so they can be matched against deploys, restarts and the backend's logs. HostTracker runs such checks from 300+ checkpoints in 158 cities and alerts by email, SMS, voice call, Slack, Telegram and more; see distributed availability monitoring.

Sources and further reading

Check it now

Run the free check against your own site - no account needed.

HTTP check

Monitor this permanently

Get alerted the moment it breaks: HostTracker checks from 300+ locations and notifies you by email, SMS, Slack, Telegram and more.

HostTracker features

More in this section: HTTP status codes explained