500 Internal Server Error
Server Error
The server encountered an unexpected condition that prevented it from fulfilling the request. 500 is intentionally generic. It does not commit to the cause, and the response body should not leak stack traces or internal paths to clients. A well-built 500 handler logs the full error with a request ID server-side and returns the request ID to the client so support and engineering can correlate later. 500 is the right code for unhandled exceptions, database connection failures during a request, and any condition where the server can no longer reason about whether the request would have succeeded.
When does this happen?
Return 500 when an unexpected condition fires that the application did not anticipate. The canonical trigger is an unhandled exception in business logic. A null dereference, a divide-by-zero, an unexpected schema mismatch. Other causes include database connection failures mid-request, out-of-memory conditions, panics in lower-level code, and assertion failures. The response should be structured so clients can detect the error programmatically (an error code or request ID) without exposing the stack trace publicly. Search engines treat 500 as a transient condition by default. They will retry the URL. But persistent 500s on indexed pages eventually cause de-indexing. Browsers display a generic error or whatever HTML the server happened to return. Watch for 500s emitted by middleware that runs before your application. Load balancers, WAFs, and reverse proxies sometimes return 500 when they cannot reach the application; the cause is upstream of your code.
Common causes
- Unhandled exception in application code. A null pointer dereference, a missing case in a switch, an unexpected type.
- Database connection pool exhausted. Every connection is in use and the request handler timed out waiting.
- Database query failed. Invalid SQL emitted by a buggy migration, a missing column, or a deadlocked transaction.
- Out of memory. The process tried to allocate more than the OS would grant and the runtime aborted.
- Disk full. The application could not write a temp file, a log line, or an uploaded asset.
- Misconfigured environment variable. A required secret is missing or contains a malformed value.
- Race condition triggered by concurrent requests. Works on dev, fails in production under load.
- Third-party dependency raised an exception the wrapper code did not catch.
- Panic in a Go goroutine, an unhandled promise rejection in Node, or an unwind in Rust.
How to fix it
- Check application logs for the stack trace. 500s almost always have an exception logged in the same request id.
- Reproduce locally with the same payload. Many 500s are deterministic and a unit or integration test will surface the bug.
- Roll back the most recent deploy if 500s spiked after a release. Bisecting in production is cheaper than debugging blind.
- Inspect connection-pool metrics. Exhausted pools manifest as 500s under load.
- Add a global exception handler that catches everything and returns a structured 500 with a request ID, so support has something to correlate.
- Audit error paths for partial writes. A request that died after writing half its data leaves inconsistent state.
- Wrap third-party calls in try/catch and decide whether to surface them as 500 or as a more specific code (e.g. 502 or 503).
- If 500 is intermittent, capture distributed traces. The cause is often in a downstream service even though the symptom appears in yours.
Real-world examples
- New code path divides by a count that turns out to be zero in production.
- Application throws ZeroDivisionError, returns 500 with a request ID. Logs show the stack trace pointing at the exact line.
- Database connection pool is sized at 20 and a slow query has tied up every connection for two minutes.
- New requests wait for a connection, eventually time out, and the handler returns 500. Operator scales up the pool or kills the slow query.
- Migration added a column but the production deploy of the application code happened before the migration ran.
- Queries reference the missing column and the database raises an error. Application returns 500 until the migration completes.
- Disk fills up at 3am because log rotation broke.
- Every request that tries to write a log line fails and the application returns 500. Operator clears space and rotates logs.
- Unhandled promise rejection in a Node service causes the request handler to never resolve.
- Framework's catch-all middleware fires and returns 500. Engineer adds explicit error handling in the offending handler.
- Race condition in a caching layer corrupts a value once per ten thousand requests.
- Affected requests deserialize the corrupted value, throw an exception, and return 500. Distributed traces show the corruption originating in the cache write.
Debugging
- Check the response body for a request ID, then grep logs for that ID to find the originating stack trace.
- Run curl -v https://api.example.com/path and confirm the 500 is from your application, not from a proxy in front of it.
- Inspect APM dashboards (Sentry, Datadog, New Relic) for grouped exceptions matching the failing endpoint.
- Reproduce the failing payload locally. Pipe the request body through curl -X POST -d @body.json and watch the application logs.
- If 500s spike during deploys, compare error counts across versions and roll back if the new version is responsible.
How it differs from related codes
HTTP 502
500 means the application itself crashed or errored. 502 means a gateway or proxy could not get a valid response from an upstream. The upstream may have crashed but the symptom is at the gateway layer.
HTTP 503
503 is a deliberate "we are unavailable". Maintenance, overload, planned downtime. 500 is an unexpected error. 503 is recoverable on its own; 500 needs investigation.
HTTP 504
504 is a timeout against an upstream server. 500 is a generic application error. If your app called another service that did not respond, the error you saw was probably 504 internally, surfaced as 500 to the client.
Related status codes
See HTTP 500 in your redirect chains?