A 502 Bad Gateway is never a problem with your browser or your device. It's one server telling you that another server behind it let it down.
Modern sites are a chain: browser → CDN → load balancer → nginx → PHP-FPM or your app. Any link in that chain that works as a proxy can return a 502 when the next link fails. The fix is to find which link failed, and the logs tell you that in one line.
Every nginx case below was reproduced on Ubuntu 24.04 with nginx 1.24 and PHP 8.3, plus nginx 1.30 in Docker. The log lines are real output, not paraphrased.
What a 502 Bad Gateway error means
A server that works as a gateway or proxy received an invalid response from the upstream server it forwarded your request to. That's the official definition (see MDN on 502).
In practice, nginx and load balancers also return 502 when they can't open a connection to the upstream at all, or when the upstream drops the connection halfway. So a 502 usually means one of three things:
- the backend isn't running or isn't listening where the proxy expects it;
- the backend accepted the connection and then closed or reset it;
- the backend answered with something the proxy couldn't accept, like headers that are too big.
The message looks different depending on who generated it:
502 Bad Gatewaywithnginxat the bottom of a plain white page;HTTP Error 502 - Bad Gatewayor justHTTP 502;502 Proxy Errorfrom Apache;- a Cloudflare-branded page with "Bad gateway" and a Ray ID;
- a bare
502 Bad Gatewayfrom a cloud load balancer.
The footer or branding is your first clue. It names the server that gave up, not the one that's broken.
502 vs 503 vs 504
These three get mixed up all the time, and they point to different fixes:
- 502 Bad Gateway. The proxy reached out to the upstream and got a refusal, a reset or a broken response. Look for a dead backend, a wrong socket or port, a crash, or oversized headers.
- 503 Service Unavailable. The server says it can't take the request right now, on purpose. Typical causes are maintenance mode, rate limiting, or a load balancer with no healthy targets.
- 504 Gateway Timeout. The proxy connected to the upstream and waited, but no answer came before the timeout. Look for slow queries, slow external APIs, or a backend that's overloaded.
Here's the difference in one test. The same PHP-FPM pool with only 2 workers was hit with 6 slow requests. With a short fastcgi_read_timeout, the requests that waited too long for a free worker got a 504, not a 502:
shell
1upstream timed out (110: Connection timed out) while reading response header from upstreamThat matters because "server overloaded" is often named as the main cause of 502. Overload usually shows up as slowness and 504s first. It turns into a 502 when the backend starts refusing or dropping connections.
If you're just visiting the site
The error isn't on your side, and nothing on your computer caused it. You can still try a few things:
- Wait a minute and reload. Many 502s happen during a deploy or a restart and go away in seconds.
- Try another network or device. If the site works on mobile data but not on your Wi-Fi, your network or DNS may be sending you to an old server.
- Clear the cache for that site. This only helps when your browser keeps showing a cached copy of the error page.
- Flush your DNS cache if the site recently moved to a new server. On Windows, run
ipconfig /flushdnsin Command Prompt. On macOS, run this in Terminal:
shell
1sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponderIf it still fails, the site owner has to fix it. Telling them the time and the exact page helps them find the request in their logs.
If you own the site: start with the error log
Don't restart things at random. Find out which hop returned the 502, then read its log.
First, check who answered. The Server header names the proxy that generated the error:
shell
1curl -sI https://example.com/ | grep -iE '^(HTTP|server|cf-ray)'Server: nginx means your own nginx gave up on PHP-FPM or the app. server: cloudflare with a cf-ray header means the request went through Cloudflare (see the Cloudflare section to tell who generated it).
On the server, the nginx error log names the exact reason:
shell
1sudo tail -n 50 /var/log/nginx/error.logTo follow it live while you reload the page:
shell
1sudo tail -f /var/log/nginx/error.logMatch the line you see to the cause:
connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)- PHP-FPM is stopped, failed to start, or nginx points to the wrong socket.connect() to unix:... failed (13: Permission denied)- nginx can't access the PHP-FPM socket.connect() to unix:... failed (11: Resource temporarily unavailable)- PHP-FPM is out of workers and its queue is full.recv() failed (104: Connection reset by peer)- PHP-FPM killed the worker in the middle of the request.upstream sent too big header while reading response header from upstream- the response headers don't fit into nginx buffers.connect() failed (111: Connection refused) while connecting to upstream- nothing listens on the host and port inproxy_pass.upstream prematurely closed connection while reading response header from upstream- the app closed the connection without answering, often because it crashed.upstream timed out (110: Connection timed out)- that's a 504, not a 502. The backend is too slow.
The sections below go through each one with the fix.
Nginx and PHP-FPM: WordPress, Laravel and other PHP sites
Nginx doesn't run PHP itself. It passes PHP requests to PHP-FPM through a Unix socket or a TCP port, set by fastcgi_pass. Most PHP 502s come from that handoff.
The commands use PHP 8.3, the default on Ubuntu 24.04. If you run another version, change 8.3 in paths and service names.
PHP-FPM is stopped or failed to start
Error log:
shell
1[crit] connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory) while connecting to upstreamWhen PHP-FPM stops, it removes its socket, so nginx has nothing to connect to. Check the service and the socket:
shell
1sudo systemctl status php8.3-fpm2ls -l /run/php/Before you restart, test the config. A typo in a pool file is a common reason PHP-FPM won't come back:
shell
1sudo php-fpm8.3 -tFor example, lowering pm.max_children below pm.max_spare_servers stops the pool from starting at all:
shell
1ALERT: [pool www] pm.min_spare_servers(1) and pm.max_spare_servers(3) cannot be greater than pm.max_children(2)2ERROR: FPM initialization failedOnce the test passes, restart it and check why it stopped:
shell
1sudo systemctl restart php8.3-fpm2sudo journalctl -u php8.3-fpm --since "1 hour ago"If PHP-FPM keeps dying, look for the out-of-memory killer in the system log (sudo journalctl -k | grep -i oom). That's a capacity problem, not a config one.
Nginx points to the wrong socket
You'll see the same (2: No such file or directory) error even though PHP-FPM is running. This happens after a PHP upgrade: the pool now listens on php8.3-fpm.sock, but the nginx config still says php8.1-fpm.sock.
shell
1[crit] connect() to unix:/run/php/php8.1-fpm.sock failed (2: No such file or directory) while connecting to upstreamCompare what nginx uses with what PHP-FPM listens on:
shell
1sudo grep -rn fastcgi_pass /etc/nginx/sites-enabled/2sudo grep -rn '^listen =' /etc/php/8.3/fpm/pool.d/The two paths must match exactly. Fix fastcgi_pass in your server block:
nginx
1location ~ \.php$ {2 include snippets/fastcgi-php.conf;3 fastcgi_pass unix:/run/php/php8.3-fpm.sock;4}If your pool listens on TCP (listen = 127.0.0.1:9000), then fastcgi_pass must be 127.0.0.1:9000 too. Test and reload:
shell
1sudo nginx -t && sudo systemctl reload nginxPermission denied on the PHP-FPM socket
Error log:
shell
1[crit] connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied) while connecting to upstreamThe socket exists, but the nginx user (www-data on Ubuntu) isn't allowed to open it. Check the owner:
shell
1ls -l /run/php/php8.3-fpm.socksrw-rw---- 1 root root is the broken state. In the pool file /etc/php/8.3/fpm/pool.d/www.conf, set the owner to the nginx user:
shell
1listen.owner = www-data2listen.group = www-data3listen.mode = 0660Restart PHP-FPM, and the socket comes back as srw-rw---- 1 www-data www-data. If you run nginx as a different user, use that user here.
Upstream sent too big header
Error log:
shell
1[error] upstream sent too big header while reading response header from upstreamNginx reads the response headers from PHP into one buffer. By default it's one memory page, 4 KB or 8 KB. Big cookies, long redirect URLs or debug headers can overflow it. WordPress plugins that set many cookies are a classic trigger.
Raise the buffers in the PHP location:
nginx
1location ~ \.php$ {2 include snippets/fastcgi-php.conf;3 fastcgi_pass unix:/run/php/php8.3-fpm.sock;4 fastcgi_buffer_size 32k;5 fastcgi_buffers 16 16k;6}⚠️ Change both lines. If you raise only fastcgi_buffer_size, nginx -t fails:
shell
1[emerg] "fastcgi_busy_buffers_size" must be less than the size of all "fastcgi_buffers" minus one bufferThen run sudo nginx -t && sudo systemctl reload nginx. If the headers keep growing, find what sets them. Bigger buffers treat the symptom.
PHP-FPM ran out of workers
Each PHP-FPM worker handles one request at a time. When all of them are busy, new requests wait in the socket queue. The PHP-FPM log tells you when you hit the limit:
shell
1sudo grep max_children /var/log/php8.3-fpm.logshell
1WARNING: [pool www] server reached pm.max_children setting (2), consider raising itWhat the visitor gets depends on how long the queue is:
- requests wait and the site gets slow;
- if they wait longer than
fastcgi_read_timeout(60 seconds by default), nginx returns a 504; - if the queue itself is full, nginx can't even connect and returns a 502:
shell
1[error] connect() to unix:/run/php/php8.3-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstreamThe fix is more workers, sized to your memory. Measure how much RAM one worker uses under real traffic:
shell
1ps -C php-fpm8.3 -o rss=,cmd= | awk '/pool/ {s+=$1; n++} END {printf "%d workers, avg %.0f MB\n", n, s/n/1024}'Divide the memory you can give PHP by that number. For example, 2 GB free for PHP and 60 MB per worker gives about 30 workers. Set it in www.conf:
shell
1pm = dynamic2pm.max_children = 303pm.start_servers = 64pm.min_spare_servers = 45pm.max_spare_servers = 10Keep pm.min_spare_servers and pm.max_spare_servers below pm.max_children, run sudo php-fpm8.3 -t, then restart. If you set more workers than your RAM can hold, the OOM killer takes PHP-FPM down and you're back to the first case.
More workers only buy time if requests are slow because of a slow database or an external API. Fix those too.
PHP-FPM killed the request
Error log in nginx:
shell
1[error] recv() failed (104: Connection reset by peer) while reading response header from upstreamAnd in the PHP-FPM log:
shell
1WARNING: [pool www] child 615, script '/var/www/html/slow.php' (request: "GET /slow.php?s=5") execution timed out (3.006784 sec), terminatingrequest_terminate_timeout in the pool kills any request that runs longer than its value, and nginx gets a reset connection. A worker that crashes on a fatal error or a segfault gives you the same picture.
If a heavy page (an import, a report, a WooCommerce export) legitimately needs more time, raise request_terminate_timeout together with fastcgi_read_timeout in nginx. Otherwise find the slow code: the PHP-FPM slow log (slowlog and request_slowlog_timeout in the pool) shows the stack of requests that take too long.
Nginx as a reverse proxy: Node, Python, Java and Docker apps
Here nginx forwards requests over HTTP with proxy_pass. The app is a separate process or a separate container, and most 502s come from nginx not finding it. For a full Java example, see how to run Tomcat behind Nginx next to WordPress on one Ubuntu server.
The app isn't running or listens on another port
Error log:
shell
1[error] connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: , request: "GET /app/ HTTP/1.1", upstream: "http://127.0.0.1:3000/", host: "localhost"The upstream: part tells you where nginx tried to go. Check what actually listens there:
shell
1sudo ss -ltnp | grep 3000No output means the app is down or listens on another port. Start it, or check the port in its config. Then make proxy_pass match:
nginx
1location / {2 proxy_pass http://127.0.0.1:3000;3}If the app was running and then disappeared, its own logs tell you why (journalctl -u your-app, docker logs, pm2 logs).
The app listens on 127.0.0.1 inside a Docker container
You run nginx in one container and the app in another, and you get this:
shell
1[error] connect() failed (111: Connection refused) while connecting to upstream, ... upstream: "http://172.21.0.2:3000/"The app container is up, but the app inside listens on 127.0.0.1. Inside a container, that address belongs only to the container, so nothing outside it can connect. Make the app listen on all interfaces, 0.0.0.0. For example, --host 0.0.0.0 for most dev servers, or app.listen(3000, '0.0.0.0') in Node.
The same mistake happens the other way. proxy_pass http://127.0.0.1:3000 inside the nginx container points to nginx itself, not to your app. Use the service or container name on a shared Docker network. If the app runs on the host machine, see how to connect to the Docker host from a container. For publishing ports, see how to expose a port in a Docker container.
The app container was recreated and nginx still uses the old IP
Everything worked, you redeployed the app container, and now you get (111: Connection refused) or a timeout. The upstream: address in the log is the old container IP.
Nginx resolves proxy_pass http://app:3000 once, when it starts, and keeps that IP. A recreated container can get a new IP. The quick fix is to restart nginx:
shell
1docker restart nginxThe lasting fix is to let nginx re-resolve the name through Docker's DNS. A variable in proxy_pass makes nginx look it up at request time:
nginx
1server {2 listen 80;3 resolver 127.0.0.11 valid=10s;4
5 location / {6 set $app_upstream http://app:3000;7 proxy_pass $app_upstream;8 }9}In the test, nginx picked up the new container IP within 2 seconds, with no restart. One side effect of the plain proxy_pass http://app:3000 form: if the name doesn't resolve when nginx starts, nginx refuses to start with host not found in upstream "app".
The app closes the connection or sends huge headers
If the app crashes or closes the socket before it answers, you'll see:
shell
1[error] upstream prematurely closed connection while reading response header from upstreamCheck the app's logs for a crash, an out-of-memory kill or a worker timeout at that moment. Gunicorn, uWSGI and PM2 all have their own worker timeouts that can kill a slow request.
The too-big-header case also exists for proxied apps, with the same log line as PHP. Use the proxy_ buffers instead of the fastcgi_ ones:
nginx
1location / {2 proxy_pass http://127.0.0.1:3000;3 proxy_buffer_size 16k;4 proxy_buffers 8 16k;5}If you see upstream timed out (110: Connection timed out), the client got a 504. Raise proxy_read_timeout only if the slow response is expected. Otherwise, make the app answer faster.
AWS Application Load Balancer 502
A 502 from an ALB is rarely about "too much traffic". According to the ALB troubleshooting guide, the load balancer returns 502 when:
- the target answers the connection with a TCP reset, often because nothing listens on the target port;
- the connection gets an ICMP "host unreachable", so traffic from the load balancer subnets to the target port is blocked;
- the target closes a connection while the ALB still has a request on it, usually because the target's keep-alive timeout is shorter than the ALB idle timeout;
- the response is malformed, or the response headers are bigger than 32 KB in total;
- a target was deregistered and the deregistration delay ran out before its request finished;
- the TLS handshake to an HTTPS target fails;
- a Lambda target errors, times out, gets throttled, or returns a body over 1 MB.
Work through it in this order.
1. Check who generated the 502
In CloudWatch, compare HTTPCode_ELB_5XX_Count with HTTPCode_Target_5XX_Count for the load balancer. If the target metric grows, your app or its nginx returned the 502 and the ALB only passed it on. Debug it as a normal nginx case on the instance. If only the ELB metric grows, the load balancer itself couldn't get a valid answer.
2. Turn on access logs
ALB access logs are disabled by default and go to an S3 bucket you choose. In each entry, look at two fields:
elb_status_codeis what the client got;target_status_codeis what the target sent.
elb_status_code 502 with target_status_code set to - means the target never sent a response. It refused, reset or closed the connection. A -1 in target_processing_time points the same way.
3. Make the app's keep-alive longer than the ALB idle timeout
This is the most common ALB 502 in production, and it happens at random, a few requests per hour. The ALB keeps connections to targets open and reuses them. The default idle timeout is 60 seconds. AWS recommends setting your application's idle timeout higher than the load balancer's. Otherwise the app can close a connection at the moment the ALB sends a new request on it, and the client gets a 502.
- Nginx on the target uses
keepalive_timeout 75sby default, which is fine. Don't lower it below the ALB idle timeout. - Node closes idle connections after 5 seconds by default (
server.keepAliveTimeoutis 5000 ms on current LTS). Behind an ALB, raise it, and keepheadersTimeouta bit higher:
typescript
1const server = app.listen(3000);2server.keepAliveTimeout = 65000;3server.headersTimeout = 66000;Other app servers have the same setting under a different name. Check its default before you blame the load balancer.
4. Check the network path and target port
The target's security group must allow traffic from the ALB's security group on the target port. The network ACLs of both subnets must allow it too, including return traffic on ephemeral ports 1024-65535. While you're there, make sure the app listens on the same port that's registered in the target group. A failing health check usually tells you this first.
5. Check deploys and headers
If 502s show up only during deployments, raise the deregistration delay on the target group so in-flight requests can finish. Make sure your app shuts down gracefully instead of dropping open connections. If only some pages fail, check their response header size against the 32 KB limit.
Elastic Beanstalk environments usually run nginx on each instance behind the load balancer. If the target metric grows there, read /var/log/nginx/error.log on the instance and use the nginx sections above.
Cloudflare 502 and errors 520-526
When Cloudflare sits in front of your site, look at the error page first. Cloudflare's own 502/504 guide gives the rule:
- A Cloudflare-branded 502 page means your origin server returned the 502, and Cloudflare passed it on. The problem is on your server, so use the nginx sections above.
- A blank 502 page without Cloudflare branding means the error came from Cloudflare itself. That's often brief, during traffic rerouting between data centers. If it persists, contact Cloudflare support with the time, the URL and the output of
/cdn-cgi/traceon your domain.
To confirm the origin is at fault, call it directly and bypass Cloudflare. Replace 203.0.113.10 with your origin IP:
shell
1curl -sI --resolve example.com:443:203.0.113.10 https://example.com/If that also returns 502, fix the origin first.
Cloudflare also has its own 52x codes. They aren't 502s, but people search for them together because they mean "Cloudflare couldn't get a good answer from your server":
- 520 - the origin returned an empty, unknown or unexpected response. Common reasons are a crashed backend, a firewall blocking Cloudflare IPs, or response headers over 128 KB (details).
- 521 - the origin refused the connection. The web server is down, or something on the server blocks Cloudflare IP ranges (details).
- 522 - the connection to the origin timed out. That's often a firewall dropping Cloudflare traffic, an overloaded server, or a wrong origin IP in DNS (details).
- 523 - the origin is unreachable, usually a wrong IP or a routing problem.
- 524 - Cloudflare connected, but the origin didn't send an HTTP response in time (125 seconds by default). This is Cloudflare's version of a 504 (details).
- 525 - the SSL handshake between Cloudflare and the origin failed.
- 526 - the origin certificate is invalid, which happens in Full (strict) SSL mode.
For diagnosis, you can switch the DNS record to DNS only (grey cloud) for a few minutes and test the site without Cloudflare. Switch it back as soon as you know the answer. Leaving the origin exposed is a security problem of its own.
WordPress-specific checks
On WordPress, the 502 still comes from nginx, PHP-FPM or the host, so start with the log sections above. When the log points at PHP (reset connections, timeouts, workers running out), a plugin is often what makes requests slow or heavy. To find it:
- deactivate all plugins with WP-CLI,
wp plugin deactivate --all, or rename thewp-content/pluginsfolder over SSH or SFTP; - if the site comes back, turn plugins on one by one until the 502 returns;
- do the same with the theme by switching to a default one.
Security and caching plugins, and a WAF in front of the site, can also cause 502s. Pause them only to test, find the one that's at fault, and turn protection back on right away. For the long-term setup, see our WordPress security hardening guide.
On shared hosting you usually can't read the nginx or PHP-FPM logs. Send your host the time of the error and the URL, and ask for the matching line from the error log. That line is worth more than any guesswork.
How to keep 502s from coming back
Most 502s are a symptom of something you can watch for:
- Alert on the 5xx rate, not just on uptime. A few 502s an hour can mean a keep-alive mismatch long before the site goes down.
- Watch PHP-FPM. Turn on
pm.status_pathin the pool and graph active workers and the listen queue, so you raisepm.max_childrenbefore visitors notice. - Test configs before reloads. Run
nginx -tandphp-fpm8.3 -tin your deploy pipeline, so a typo never reaches production. - Deploy gracefully. Use health checks, a deregistration delay and graceful shutdowns, so deploys don't cut off requests that are still running.
We cover what to monitor and how to set up alerts in our guide to monitoring system organization.
If 502s keep coming back and the logs point in different directions, the cause is usually in the architecture: timeouts that don't match across layers, too little capacity, or deploys without draining. Our engineers can review your setup end to end and give you a clear plan to fix it. Start with a cloud infrastructure review.
