Home › DevOps › 502 Bad Gateway in Nginx: Fix the Failing Upstream
Intermediate 7 min · September 23, 2026

502 Bad Gateway in Nginx: Fix the Failing Upstream

Restart the crashed backend to clear an Nginx 502 fast.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 11 min
  • ✓Basic HTTP and terminal comfort
  • ✓Access to Nginx config and logs
  • ✓A backend service running behind the proxy
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Fix the backend first: restart the crashed app server, then reload Nginx, since most 502s clear the moment the upstream answers again
  • A 502 means Nginx got an invalid or empty reply from its upstream, so the fault usually sits behind the proxy rather than in it
  • Confirm with a direct curl against the backend port plus tail -f /var/log/nginx/error.log for connect() failed or prematurely closed
  • If the backend is healthy, check proxy_pass for a wrong port and raise proxy_read_timeout only when the app is genuinely slow
✦ Definition~90s read
What is HTTP 502 Bad Gateway Fix?

A 502 Bad Gateway is what an Nginx reverse proxy returns when it cannot get a usable response from the upstream server behind it. The request path has two hops: client to Nginx, then Nginx to your app via the proxy_pass target. The first hop worked — Nginx is up and answering — while the second hop produced a refusal, an empty reply, or bytes Nginx couldn't forward.

★
Picture a restaurant receptionist who takes your order to the kitchen.

The 502 page is Nginx honestly reporting that its errand failed, which is why the investigation starts behind the proxy rather than inside it.

The failure modes split three ways. The backend may be down entirely: crashed process, failed deploy, or OOM kill, with the kernel refusing connections on its port. The proxy may be misconfigured: proxy_pass naming a stale port, an unresolvable upstream hostname, or an unloaded config file, so a healthy app never receives the request.

Or the backend may be too slow: alive but exceeding proxy_connect_timeout, proxy_send_timeout, or proxy_read_timeout, so Nginx abandons the wait. Error.log fingerprints each one — connect() failed, prematurely closed, upstream timed out — and a direct curl to the backend port settles the split in seconds.

What a 502 is NOT completes the picture. It is not a 503, which means the service deliberately declined with overload or maintenance semantics and often a Retry-After hint. It is not a 504, which means the upstream was reachable but slower than the proxy's patience.

And it is not an Nginx failure: restarting the proxy fixes nothing when the app behind it is dead, misaddressed, or crawling. Treat the 502 as a pointer past the proxy, follow it with error.log and curl, and most incidents resolve at the backend within minutes.

Plain-English First

Picture a restaurant receptionist who takes your order to the kitchen. One evening she returns empty-handed: 502 is her telling you the kitchen didn't answer — the stove went out, the chefs moved rooms, or they're just slow. Yelling at the receptionist (restarting Nginx) never relights the stove. Check the kitchen directly: walk back there yourself (curl the backend), read her note (error.log), and fix the kitchen before retraining the receptionist's patience (timeouts).

Checkout is down. Every browser shows the same stark page: 502 Bad Gateway. Nginx is up, the servers are up, CPU graphs are flat — yet every request through the proxy dies. Someone suggests restarting Nginx. Someone else blames the CDN. Meanwhile the actual culprit, a crashed app server one hop behind the proxy, sits quietly untouched.

A 502 is Nginx telling you the truth about someone else: it asked the upstream for a response and got garbage, silence, or a slammed door. The fix is almost always behind the proxy, not in it — a dead process, a proxy_pass pointing at last month's port, or an app slower than the proxy's patience. Reading one log file correctly splits those cases in under a minute.

This guide shows how to confirm which one you have with error.log and a direct curl, fix the backend first, and only then tune proxy_pass and timeouts. You'll also learn to stop confusing 502 with its cousins 503 and 504, so your status codes tell the truth during the next incident. Every step uses commands you can run as-is.

502 Means the Proxy Got a Bad Answer From Upstream

A 502 Bad Gateway is the proxy reporting a failed conversation, not a failed proxy. Nginx accepted your request, opened a connection to the upstream named in proxy_pass, and received something it couldn't use: a refused connection, an empty reply, a truncated response, or headers so malformed they can't be forwarded. HTTP semantics pin the blame precisely — the gateway received an invalid response from the inbound server — which is why seasoned operators read a 502 as upstream trouble until proven otherwise.

That conversation can break at three distinct moments. Before it starts, when nothing listens on the configured host and port and the kernel answers with connection refused. Midway, when the app accepts the connection but crashes or hangs up early, leaving Nginx holding half a response it must discard. Or at the content layer, when the upstream sends bytes Nginx can't forward, such as headers larger than its buffers or a body that ends mid-chunk. Each moment leaves a different fingerprint in error.log, which is why that file — not access.log — is the primary witness.

The most important consequence is where you spend your first five minutes. Restarting Nginx, purging CDN caches, and tweaking DNS all target machinery that is demonstrably working: Nginx is healthy enough to produce a well-formed 502. The fault sits one hop behind it in the app process, its port binding, or its response time. Teams that internalize this aim every first command past the proxy instead of at it.

The rest of this guide makes that reflex concrete. You'll learn the three error.log phrases that split every 502 into dead, misconfigured, or slow; the direct-curl test that bypasses the proxy in seconds; minimal proxy_pass and timeout blocks that are correct without being clever; and the 502 vs 503 vs 504 distinction that keeps your status codes honest.

📊 Production Insight
First-minute instincts decide 502 incidents: teams that curl the backend first recover in minutes, while teams that restart Nginx first donate an extra half hour to a healthy proxy.
🎯 Key Takeaway
Nginx is healthy enough to complain, so the fault is almost always behind it. Spend your first minutes interrogating the upstream, not restarting the proxy.

Backend Down vs Proxy Misconfigured: Tell Them Apart

Every 502 investigation starts with one split: is the backend dead, or is the proxy misdirected? The direct curl answers it in seconds. Point curl at the backend's host and port with the proxy removed from the path — a 200 means the app is alive and Nginx is looking in the wrong place, while connection refused means the process is down regardless of what any config says. This single test eliminates half the hypothesis space before you've opened a single config file.

A dead backend usually announces itself loudly once you look. systemctl status shows inactive or failed with the last few log lines pointing at the crash; docker ps shows the container missing or restarting, and docker logs ends with an out-of-memory kill or an unhandled exception. These are application or capacity problems wearing a proxy costume. Fix the crash, the leak, or the failed deploy — no Nginx directive addresses a process that isn't running.

A living backend behind a persistent 502 points the other way: Nginx is asking the wrong address. Compare the port in proxy_pass against the port the app actually bound, which ss -ltnp reports authoritatively. Deploys that move ports, hosts files that changed, and upstream blocks copied between environments all produce a healthy app and a 502 at once. The proxy is faithfully forwarding to nowhere.

Make the order a habit: curl the backend, check the process, then read the config. Skipping straight to config edits while the process is dead wastes the exact minutes when checkout is failing. The commands above fit in one terminal and run in under thirty seconds, which is the right budget for triage when every checkout returns 502.

bypass-proxy.shBASH
1
2
3
4
5
6
7
8
9
# Ask the backend directly, bypassing Nginx entirely
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3001/health

# Is the app process even alive?
systemctl status myapp --no-pager

# Or for containers: running state plus recent crash output
docker ps --filter name=myapp
docker logs --tail 50 myapp
📊 Production Insight
Port drift after deploys is the classic misdirected proxy: the app moves to 3001, the config still says 3000, and every dashboard except error.log looks perfectly healthy.
🎯 Key Takeaway
Direct curl plus a process check splits every 502 into dead backend or wrong address in under thirty seconds. Fix the process before editing any proxy config.

Read Nginx error.log Like a Map to the Fault

Nginx's error.log is the authoritative witness for every 502, and three phrases cover nearly all of them. connect() failed (111: Connection refused) while connecting to upstream means nothing listened at the configured address — dead process or wrong port, settled by the direct curl. upstream prematurely closed connection means the app accepted then hung up mid-response: a crash between accept and reply, a response larger than buffers, or a keepalive race. upstream timed out means the app never finished in time, which is slowness rather than death.

Each phrase demands a different next move, which is why reading before acting matters. Refused sends you to the process list and the port comparison. Premature close sends you to app crash logs and response sizes around the failing endpoint. Timed out sends you to latency measurements and timeout math, never to a restart. Teams that grep the phrase first pick the right fix on the first try; teams that guess restart first sometimes never learn which fault they had.

The config commands beside the log matter just as much. nginx -t validates syntax without touching traffic, catching the missing semicolon before it becomes an outage. nginx -T dumps the effective running configuration — the files Nginx actually loaded, includes resolved — so grep on proxy_pass shows the true targets instead of the file you wish were loaded. When the on-disk edit and the -T output disagree, you've found the sites-enabled symlink or include-path problem.

Build the reflex as tail, grep the phrase, dump the live targets. Those three outputs — the failure mode, the claimed target, the real target — frame every 502 completely. Everything after them is repair, and repair without that frame is how one-digit typos survive for 38 minutes.

read-error-log.shBASH
1
2
3
4
5
6
7
8
# Fresh upstream errors with context, newest last
sudo tail -n 100 /var/log/nginx/error.log | grep -E 'upstream|connect\(\)|timed out'

# Validate syntax before touching anything live
sudo nginx -t

# Print the running config's proxy targets
sudo nginx -T | grep -B 2 -A 2 proxy_pass
💡Three Phrases Split Every 502
Grep for the three phrases, not the status code: connect() failed means dead or misaddressed, prematurely closed means the app hung up, and timed out means too slow. The phrase is the diagnosis.
📊 Production Insight
Access logs record that a 502 happened; only error.log records why. Alerting on error.log upstream phrases pages the right team while status-code alerts just count the damage.
🎯 Key Takeaway
The error.log phrase names the fault class and nginx -T names the live target. Collect both before changing anything, and the repair becomes obvious.

Fix proxy_pass and Upstream Blocks Without Guessing

The minimal correct proxy block has four ingredients: an upstream naming the real host and port, a location forwarding to it, and the two headers backends need for correct URLs and client IPs. The upstream block earns its place the day you add a second app server — one name to change instead of five location blocks. Until then it documents intent: this name is the app, and the app lives here.

Port accuracy is the whole game. The port in the upstream or proxy_pass line must equal the port the app bound, verified with ss -ltnp rather than memory. Deploys move ports, environment copies preserve stale ones, and typos turn one digit into a total outage for that location. Render the port from the same variable or service discovery entry the app uses so the two can never drift again — a single source of truth beats any amount of careful typing.

Watch the trailing-slash semantics when you write proxy_pass with a URI part. proxy_pass http://app_backend; under location /api/ preserves the full path, while proxy_pass http://app_backend/v1/; strips the matched prefix and rewrites it. Both are correct in different designs; mixing them up converts a fixed 502 into a wave of 404s. Test one real endpoint path with curl after every proxy change and compare what the app's access log received against what you sent.

Apply changes with nginx -t followed by nginx -s reload, then confirm with nginx -T that the live config matches your edit. The reload swaps configs without dropping connections, and the -T dump closes the loop between intention and reality. If 502s persist past a correct reload, the fault was never the config — return to the backend, because a perfect proxy still 502s against a dead app.

/etc/nginx/sites-available/appNGINX
1
2
3
4
5
6
7
8
9
10
11
12
13
upstream app_backend {
    server 127.0.0.1:3001;
}

server {
    listen 80;

    location /api/ {
        proxy_pass http://app_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
⚠ Reload, Don't Restart
A reload is not a restart: nginx -s reload applies config with zero dropped connections, while restart kills them. Validate with nginx -t first, because a reload with a broken config keeps the old one and lies about it.
📊 Production Insight
The deadliest proxy_pass edits are almost right: correct host, port off by one digit, all health checks green except the one location nobody smoke-tested after reload.
🎯 Key Takeaway
Name the real host and port once, forward it cleanly, respect trailing-slash rewrite rules, and confirm the live config after reload. Then re-test through the proxy.

Timeouts vs Slow Apps: Tune Without Hiding Outages

Nginx carries three separate patience knobs, and each guards a different phase. proxy_connect_timeout caps the TCP handshake with the upstream — keep it short, around seconds, so dead backends fail fast instead of piling up connections. proxy_send_timeout caps pushing the request body uphill, which matters for uploads. proxy_read_timeout caps the wait between bytes of the response, and it is the one slow endpoints actually hit.

The order of operations is non-negotiable: prove the app slow before extending patience. An upstream timed out line plus direct curls that eventually succeed is the evidence — the backend answers, just slower than the current read timeout. Raising proxy_read_timeout for that endpoint, scoped to its location block as shown here, is then a deliberate capacity decision. Raising it globally because one report endpoint is slow grants every endpoint patience nobody reviewed.

Never tune timeouts over a dead backend. Extending read timeouts while error.log shows connect() failed converts instant, debuggable 502s into connections that hang for two minutes before failing identically. Users experience it as the site freezing instead of erroring, and your connection pools fill with doomed waits. Timeouts shape slow success; they cannot create success from refusal.

Prefer fixing the app's latency over extending the proxy's patience. A report endpoint taking 90 seconds usually needs pagination, caching, or a background job — not a 120s read timeout blessed forever. When slowness is genuinely legitimate, scope the generous timeout to that location, alert on its latency separately, and revisit it quarterly so temporary patience doesn't fossilize into permanent debt.

/etc/nginx/sites-available/appNGINX
1
2
3
4
5
6
location /api/reports/ {
    proxy_pass http://app_backend;
    proxy_connect_timeout 10s;
    proxy_send_timeout 60s;
    proxy_read_timeout 120s;
}
📊 Production Insight
Global timeout raises are outage camouflage: they turn crisp 502s into hanging requests that exhaust connection pools, converting a visible failure into a cascading one.
🎯 Key Takeaway
Connect short, send normal, read generous only where proven slow — and fix app latency before blessing it with timeout. Timeouts describe slow success, never dead backends.

502 vs 503 vs 504: Return the Status That Tells the Truth

These three statuses describe three different truths, and returning the wrong one sends the next incident down the wrong path. A 502 says the upstream answered badly: refused connection, truncated reply, or garbage headers. It indicts the conversation between proxy and app, and its remedy lives in process health or proxy targets. Monitoring that counts 502s is counting broken conversations.

A 503 says the service knowingly declined: overloaded, in maintenance, or shedding load, ideally with a Retry-After header telling clients when to return. It indicts capacity or intent, and its remedy is scaling, draining, or waiting. Configure your maintenance pages and overload shedding to return 503 deliberately — a load balancer that sees 503 retries elsewhere, while one that sees 502 may keep hammering a dead conversation.

A 504 says the upstream never answered in time: the proxy waited the full proxy_read_timeout and gave up. It indicts slowness, and its remedy is faster handlers or deliberately longer waits. The error.log distinction is crisp — prematurely closed or refused versus upstream timed out — so classify from the log line rather than the status code when they seem to disagree.

Get this taxonomy into your runbooks and your app code. Backends should return 503 with Retry-After when they choose to decline, letting the proxy forward honesty instead of manufacturing a 502 from a timeout. Review 502 and 504 trends separately in dashboards: 502s spiking means broken deploys or dead processes, while 504s climbing means growing latency. One graph blending them hides both stories at once.

📊 Production Insight
Blended 5xx dashboards are incident camouflage: a 502 spike from a bad deploy hides inside normal 504 noise, delaying the rollback by the exact length of the confusion.
🎯 Key Takeaway
502 means bad reply, 503 means deliberate refusal, 504 means too slow. Log the distinction, alert on each separately, and have backends emit honest 503s.
● Production incidentPOST-MORTEMseverity: high

A One-Digit Port Typo Returned 502 on Checkout for 38 Minutes

Symptom
Within a minute of the 11:02 deploy, 100% of /api/checkout requests returned 502 while the homepage served fine. The outage lasted 38 minutes across roughly 4,100 failed checkouts. Nginx stayed up with flat resource graphs; only error.log told the truth with repeated connect() failed lines.
Assumption
The team assumed the deploy had broken application code, so three engineers spent 25 minutes reading a diff that was perfectly fine. Nobody looked at the proxy config because the deploy checklist called it untouched infrastructure.
Root cause
The release moved the Node API from port 3000 to 3001 while the Nginx site file kept proxy_pass http://127.0.0.1:3000. Every proxied request hit a closed port, and error.log filled with connect() failed (111: Connection refused). The app itself was healthy on 3001 the entire 38 minutes — Nginx simply never asked it.
Fix
The fix was one line plus a safe reload: proxy_pass changed from port 3000 to 3001, validated with nginx -t, applied with nginx -s reload in under a minute. Checkout recovered fully within 4 minutes of the reload. The port is now rendered from the same variable the app's systemd unit uses, and CI fails the build when nginx -T targets don't match listening ports.
Key lesson
  • Proxy targets are code: review and test proxy_pass changes with the same rigor as application diffs.
  • Alert on error.log upstream phrases, not just status codes. connect() failed pinpoints the fault 30 minutes faster than dashboards.
  • Bypass-and-compare is the fastest split in proxy incidents: one direct curl separates backend death from config drift.
Production debug guideFive checks that separate dead backends from bad proxy config in minutes, with the exact commands for each.5 entries
Symptom · 01
All or some requests return 502 through Nginx
→
Fix
Run sudo tail -n 100 /var/log/nginx/error.log | grep -E 'upstream|connect\(\)|timed out' and read the verdict. connect() failed (111: Connection refused) means dead backend or wrong port; upstream prematurely closed means the app hung up; upstream timed out means the app is too slow. The phrase picks your next step.
Symptom · 02
You can't tell whether Nginx or the backend is at fault
→
Fix
Run curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3001/health against the real backend port, replacing 3001 with your app's port. A 200 blames proxy config; a refusal blames the backend process. Add -v when you need headers and timing to see exactly where the conversation dies.
Symptom · 03
Direct curl to the backend also fails
→
Fix
Run systemctl status myapp --no-pager for systemd services or docker ps --filter name=myapp plus docker logs --tail 50 myapp for containers. Look for inactive,OOMKilled, or restart loops. Fix the crash or OOM first — no proxy setting revives a dead process.
Symptom · 04
Backend is healthy but the 502 persists
→
Fix
Run sudo nginx -t to validate syntax, then sudo nginx -T | grep -A 5 proxy_pass to print the live upstream targets. Compare the port against the one your app actually listens on via ss -ltnp | grep <port>. A mismatch of even one digit is a full outage for that location block.
Symptom · 05
error.log shows upstream timed out instead of refused
→
Fix
Run curl -w 'connect:%{time_connect} start:%{time_starttransfer} total:%{time_total}\n' -o /dev/null <backend-url> to measure the app's real response time, then compare against proxy_read_timeout in the location block. If the app is slower, fix the app first and raise the timeout only for endpoints proven legitimately slow.
502 Bad Gateway Causes Compared
Root CauseHow to ConfirmFixPrevention
Backend process crashed or never startederror.log shows connect() failed (111: Connection refused); direct curl to the port failsRestart the app service and fix the crashsystemd auto-restart plus a process-alive alert
proxy_pass points at the wrong port or hostDirect curl to the real port succeeds while Nginx logs refused on the configured oneCorrect proxy_pass and run nginx -t plus reloadRender ports from one source of truth and test config in CI
App slower than proxy timeoutserror.log shows upstream timed out; direct curl eventually succeedsSpeed up the app, then raise proxy_read_timeout deliberatelyAlert on p95 latency before it crosses the timeout
Backend OOM-killed under loadContainer exit code 137 or kernel OOM lines; docker ps shows restarts climbingRaise memory limits and fix the leak or spikeMemory alerts at 80% plus load-test before launches
Upstream closed a reused keepalive connectionerror.log shows upstream prematurely closed connection on healthy backendsAlign keepalive settings on both sides and retry idempotent requestsLoad-test with keepalive enabled and watch error.log
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
bypass-proxy.shcurl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3001/healthBackend Down vs Proxy Misconfigured
read-error-log.shsudo tail -n 100 /var/log/nginx/error.log | grep -E 'upstream|connect\(\)|timed ...Read Nginx error.log Like a Map to the Fault
etcnginxsites-availableappupstream app_backend {Fix proxy_pass and Upstream Blocks Without Guessing
etcnginxsites-availableapplocation /api/reports/ {Timeouts vs Slow Apps

Key takeaways

1
A 502 means Nginx got an invalid or empty reply from the upstream, so investigate behind the proxy first.
2
error.log phrases split the cause
connect() failed, prematurely closed, or upstream timed out.
3
Curl the backend directly to separate proxy misconfiguration from backend failure in seconds.
4
Fix the backend before tuning timeouts, or slow failures just become slower failures.
5
502 is a bad reply, 503 is knowingly unavailable, and 504 is too slow
return each deliberately.
6
Validate with nginx -t, reload cleanly, and confirm the live config with nginx -T.

Common mistakes to avoid

5 patterns
×

Raising proxy timeouts before checking whether the backend is alive

Symptom
Timeouts grow to 300s while the backend is actually crashed, turning fast 502s into slow ones that hold connections open.
Fix
Check the backend first with a direct curl to its port, then read error.log. Tune timeouts only after you prove the app is alive but genuinely slow.
×

Editing a config file Nginx never loads

Symptom
proxy_pass is correct on disk but 502s continue because the live config still points at the old port.
Fix
Edit the file under sites-available, ensure the symlink exists in sites-enabled, then run nginx -t && nginx -s reload and confirm with nginx -T.
×

Restarting Nginx instead of the backend

Symptom
Reloads report success while 502s persist, because the crashed app server was never touched.
Fix
Restart or fix the app server behind the proxy. Reloading Nginx only helps when the config changed; it can't resurrect a dead upstream.
×

Misreading proxy_pass trailing-slash behavior

Symptom
The 502 clears but every API call 404s, because the upstream now receives a rewritten path it doesn't serve.
Fix
Remember that location /api/ with proxy_pass http://host:3001; preserves the path, while a URI in proxy_pass rewrites it. Test with curl and read access logs on both sides.
×

Blaming the CDN or DNS for a backend outage

Symptom
Hours spent purging caches and checking DNS while the app server sits crashed one hop behind Nginx.
Fix
Treat a 502 as proxy-plus-upstream evidence: tail error.log for connect() failed or timed out, and curl the backend directly before blaming the edge.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does 502 Bad Gateway mean on an Nginx reverse proxy?
Q02JUNIOR
How do you tell a 502 from a 504 in error.log?
Q03SENIOR
Walk me through debugging a 502 in production.
Q04SENIOR
What do the three Nginx proxy timeouts control?
Q05SENIOR
Why would a healthy backend still cause intermittent 502s?
Q01 of 05JUNIOR

What does 502 Bad Gateway mean on an Nginx reverse proxy?

ANSWER
A 502 means the proxy received an invalid, empty, or no response from the upstream server. Common triggers: the backend crashed, proxy_pass points at the wrong port, or the app closed the connection early. The proxy itself is healthy; the conversation behind it failed.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Does a 502 mean Nginx is broken?
02
How do I test the backend without the proxy?
03
Should I just raise the proxy timeouts?
04
What is the difference between 502, 503, and 504?
05
Can a deploy cause a 502 with no code bug?
06
How do I safely apply a proxy_pass fix?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's Networking. Mark it forged?

7 min read · try the examples if you haven't

←
Previous
Docker COPY vs ADD Instruction
1 / 5 · Networking
Next
Kubernetes CrashLoopBackOff Fix
→