500 Internal Server Error
Server error response defined by RFC 9110 §15.6.1.
Written and maintained by Ben Ennis
Last reviewed July 27, 2026 · How we verify this
Common causes at a glance
- An unhandled exception escaping a request handler
- Database connection pool exhausted or the database refusing connections
- A missing or renamed environment variable after a deploy
- A worker killed by the OOM killer mid-request
Reproduce it
curl -sS -o /dev/null -w 'code=%{http_code}\n' https://api.example.com/v1/ordersWhat 500 Internal Server Error means
500 is what a server says when it has nothing useful to say. Every other 5xx code names a specific failure mode; 500 is the residue. A handler threw, a driver raised on a closed connection, a template referenced a field that was not there. The framework catches whatever escaped and emits 500 because the alternative is dropping the connection.
The operationally important property is that 500 tells the client nothing on purpose. The body must never contain a stack trace, a SQL statement, a filesystem path, or a framework debug page. Those leak directory layout, dependency versions, and sometimes credentials to anyone who can trigger the error. From the outside a leaky 500 and a clean one look identical to monitoring, which is how a staging debug renderer survives in production for months.
What the spec says
RFC 9110 §15.6.1 defines 500 in two lines: the server encountered an unexpected condition that prevented it from fulfilling the request. The surrounding §15.6 sets the class-wide rule that a 5xx server is declaring itself aware that it erred. The spec says nothing about the body, so the shape of your error payload is yours; RFC 9457 problem details is the usual answer for APIs. Unlike 501, a 500 is not heuristically cacheable.
What actually causes it
Most real 500s are application bugs reached by a request shape nobody
tested: a query parameter that is absent rather than empty, a user record with a null
field, a payload far larger than the fixture. The second cluster is environmental — a
database that hit max_connections, a secret that failed to mount, a
migration applied to one replica but not the others. If the rate steps up at a release
boundary and stays there, the deploy is the cause; if it tracks traffic, look at pool
sizes and memory limits before reading any code.
How to debug it
First establish who generated it. An application 500 carries your framework’s headers and error format; a 500 emitted by nginx after the upstream died looks generic and will not appear in your application logs at all:
curl -sS -D - -o /dev/null https://api.example.com/v1/orders | head -20
Then pull the failing request out of your logs by request ID and read the trace. Before anything else, confirm the response is not leaking internals: if the body contains a file path or a stack frame, turn the debug renderer off now. The header inspector shows whether a proxy rewrote the status on the way out.
Whether it is safe to retry depends on the method, not the 500
A 500 tells you the server gave up, not whether your write happened before it did. A
handler can throw after committing a database transaction, and the client sees the same
500 as one that threw before touching the database. RFC 9110 §9.2.2
defines PUT, DELETE, and the safe methods as idempotent because
“the request can be repeated automatically if a communication failure occurs before the
client is able to read the server’s response” — repeating them has the same intended
effect as sending them once, so an automatic retry is sound. POST is not on
that list, and the spec is explicit: a client SHOULD NOT automatically
retry a non-idempotent request unless it has some separate way to know the semantics are
safe, or some way to detect the original request never landed.
This is exactly the failure mode idempotency keys exist to close.
Stripe’s API saves the status code and
body of the first request under a given key — including a 500 — and returns that same
saved result for any retry with the same key, so a client can retry blindly after a
timeout or a 500 without risking a duplicate charge. Without a key, retrying a
POST after a 500 is a guess: if the handler failed before its side effect,
the retry is free; if it failed after, the retry duplicates it. RFC 9110 also puts a
tighter rule on intermediaries than on clients: a proxy MUST NOT
automatically retry non-idempotent requests on your behalf, precisely because it has no
way to know which case it is looking at.
Design for this before the 500 happens: make writes idempotent by construction, or accept an idempotency key, rather than deciding retry policy after the fact.
Working examples
curl
# -D - prints response headers; the Server header tells you which hop answered.
curl -sS -D - -o /dev/null https://api.example.com/v1/orders
# Loop a route to see whether the failure is deterministic or intermittent.
for i in $(seq 1 20); do
curl -sS -o /dev/null -w '%{http_code} ' https://api.example.com/v1/orders
done; echo
Python (requests)
import requests
# requests does not raise on 500; you have to check for it.
resp = requests.get("https://api.example.com/v1/orders", timeout=10)
print(resp.status_code, resp.headers.get("Server"))
if resp.status_code == 500:
# Truncate: a leaky server may be handing you a full stack trace.
print("error body:", resp.text[:400])
resp.raise_for_status() # raises requests.exceptions.HTTPError
Node (fetch)
// fetch() resolves normally on 500 — response.ok is the only signal.
const res = await fetch("https://api.example.com/v1/orders");
if (res.status === 500) {
console.error("server error from", res.headers.get("server"));
console.error((await res.text()).slice(0, 400));
} else if (!res.ok) {
console.error("other failure:", res.status);
} else {
console.log(await res.json());
}Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Related reading
- SLOs, SLIs, and error budgets: a primer
This is the kind of error you should be measuring against an error budget rather than paging on individually. How to pick an indicator and alert on burn rate.
- Every HTTP status code, and when it shows up
The wider map: which codes you actually meet in production, and the pairs everyone confuses.
Other 5xx codes
- 501 Not Implemented
- 502 Bad Gateway
- 503 Service Unavailable
- 504 Gateway Timeout
- 505 HTTP Version Not Supported
- 506 Variant Also Negotiates
- 507 Insufficient Storage
- 508 Loop Detected
- 510 Not Extended
- 511 Network Authentication Required
- 520 Web Server Returns an Unknown Error
- 521 Web Server Is Down
- 522 Connection Timed Out
- 523 Origin Is Unreachable
- 524 A Timeout Occurred
- 525 SSL Handshake Failed
- 526 Invalid SSL Certificate
- 530 Unable to Resolve Origin Hostname