Skip to content
Control Plane Labs

500 Internal Server Error

Server error response defined by RFC 9110 §15.6.1.

Last updated July 27, 2026

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/orders

What 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.

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.

Other 5xx codes