HTTP 5xx Server Error Codes — 500, 502, 503, 504 Explained
HTTP 5xx codes indicate server-side failures. Learn when 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, and 504 Gateway Timeout occur, how to handle them...
HTTP 5xx codes tell clients the server failed to fulfill a valid request. Unlike 4xx errors, 5xx errors are not the client’s fault — they indicate the server is broken, overloaded, or a dependency failed.
Use the HTTP Status Codes Reference to look up any status code.
500 Internal Server Error
The generic server error — an unhandled exception or bug:
500 — Triggered by:
- Unhandled exception in application code
- Database query throwing an unexpected error
- Null pointer / undefined property access
- Misconfigured application (wrong env vars)
- Out of memory errors
What clients should do: Not retry immediately (the error will recur). Show a user-friendly error. Log the requestId for support.
What servers should do: Never expose stack traces or internal details:
// WRONG: leaks internal details
app.use((err, req, res, next) => {
res.status(500).json({ error: err.stack });
});
// RIGHT: log internally, return generic message
app.use((err, req, res, next) => {
console.error({ err, requestId: req.id });
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
requestId: req.id,
}
});
});
501 Not Implemented
The server doesn’t support the requested functionality:
501 — Use when:
- HTTP method is not implemented at all (e.g., TRACE is disabled)
- Feature is planned but not yet built
Rarely used in practice — 405 (Method Not Allowed) is more common for unsupported methods on specific endpoints.
502 Bad Gateway
A reverse proxy or gateway received an invalid response from an upstream server:
502 — Triggered by:
- Load balancer can't connect to application server
- Nginx can't reach the upstream Node.js/Python process
- Microservice called by your API is down
- Upstream returned a malformed HTTP response
# Example: Nginx returns 502 when upstream is down:
upstream app {
server 127.0.0.1:3000;
}
server {
location / {
proxy_pass http://app;
# If app is down → 502 Bad Gateway
}
}
What clients should do: Wait and retry with exponential backoff. The upstream service is likely recovering.
503 Service Unavailable
The server is temporarily unable to handle requests — overloaded or down for maintenance:
503 — Triggered by:
- Server is under too much load
- Maintenance mode / deployment in progress
- Circuit breaker tripped to protect a failing service
- Database connection pool exhausted
HTTP/1.1 503 Service Unavailable
Retry-After: 300
Content-Type: application/json
{"error": {"code": "SERVICE_UNAVAILABLE", "message": "Temporarily down for maintenance"}}
Always include Retry-After when you know when the service will recover.
// Maintenance middleware:
app.use((req, res, next) => {
if (process.env.MAINTENANCE_MODE === 'true') {
return res.status(503)
.set('Retry-After', '600')
.json({ error: { code: 'MAINTENANCE', message: 'Under maintenance until 14:00 UTC' }});
}
next();
});
504 Gateway Timeout
The gateway/proxy didn’t receive a response from the upstream in time:
504 — Triggered by:
- Upstream service is too slow (long DB query, heavy computation)
- Network timeout between services
- Upstream service is frozen but not down (would get 502 if crashed)
# Nginx timeouts:
proxy_connect_timeout 10s; # Time to establish connection
proxy_read_timeout 60s; # Time waiting for upstream to respond
proxy_send_timeout 30s; # Time to send request
# If proxy_read_timeout exceeded → 504 Gateway Timeout
What clients should do: Retry with backoff. The operation may or may not have completed on the server (unlike 503, which definitely didn’t process it).
Retry logic for 5xx errors
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
const RETRYABLE = [500, 502, 503, 504];
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.ok) return res;
if (!RETRYABLE.includes(res.status) || attempt === maxRetries) {
throw new Error(`HTTP ${res.status}`);
}
// Exponential backoff: 1s, 2s, 4s
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
// Check Retry-After header:
const retryAfter = res.headers.get('Retry-After');
const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : delay;
await new Promise(r => setTimeout(r, waitMs));
}
}
Quick reference
| Code | Name | When it occurs | Client action |
|---|---|---|---|
| 500 | Internal Server Error | Bug, unhandled exception | Show error, no retry |
| 501 | Not Implemented | Unsupported method/feature | Don’t retry |
| 502 | Bad Gateway | Upstream unreachable | Retry with backoff |
| 503 | Service Unavailable | Overloaded, maintenance | Retry after Retry-After |
| 504 | Gateway Timeout | Upstream too slow | Retry with backoff |
Related tools
- HTTP Status Codes Reference — full status code lookup
- HTTP 4xx Client Error Codes — client error guide
- REST API Error Handling — error response design
Related posts
- When to Use 422 vs 400 (and Other HTTP Status Code Debates) — 400 means the request is malformed. 422 means it's valid but semantically wrong.…
- HTTP 301 vs 302 Redirect — Permanent vs Temporary Redirects Explained — Understand when to use 301 (permanent) vs 302 (temporary) redirects, how they af…
- HTTP 404 Not Found — What It Means and How to Fix It — HTTP 404 means the server understood the request but can't find the resource. It…
- HTTP 4xx Client Error Codes — 400, 401, 403, 404, 409, 422, 429 Explained — HTTP 4xx status codes indicate client errors. Here's when to use 400 Bad Request…
- HTTP Status Codes Reference — Complete Guide to All Response Codes — HTTP status codes tell clients what happened with their request. Here's a comple…
Related tool
Full HTTP status code reference with explanations and when to use each.
Written by Mian Ali Khalid. Part of the Dev Productivity pillar.