Which HTTP Status Code Should Your API Actually Return?
Two failure modes show up constantly in real APIs: every response is 200 OK with an { "error": "..." } body buried inside, or every failure is 500 Internal Server Error regardless of whether it was the client's fault. Both throw away information the status code exists to carry, and both make client error-handling harder than it needs to be.
The client/server split is the first decision
Before picking a specific code, decide who's at fault: 4xx means the client sent something wrong — bad input, missing auth, a resource that doesn't exist. 5xx means the server failed to handle a valid request — a database timeout, an unhandled exception, a downstream service being down. Getting this split right lets clients decide automatically whether retrying makes sense (retry on 5xx, don't retry on 4xx without changing the request).
The 4xx codes that actually matter
- 400 Bad Request — malformed syntax or invalid parameters. The request itself is broken.
- 401 Unauthorized — no valid credentials were provided at all. Despite the name, this is about authentication, not authorization.
- 403 Forbidden — valid credentials, but they don't have permission for this action. Authorization, not authentication.
- 404 Not Found — the resource doesn't exist, or you don't want to reveal that it does (avoiding a 403 that leaks existence).
- 409 Conflict — the request conflicts with current state, like creating a resource that already exists with a unique constraint.
- 422 Unprocessable Entity — syntactically valid request, but semantically invalid (a validation failure your framework didn't map to 400).
- 429 Too Many Requests — rate limit hit. Pair this with a
Retry-Afterheader so clients know when to try again.
The 5xx codes worth distinguishing
- 500 Internal Server Error — the generic catch-all for an unhandled failure. Fine as a fallback, bad as your only 5xx.
- 502 Bad Gateway — your server, acting as a proxy, got an invalid response from an upstream service.
- 503 Service Unavailable — temporarily overloaded or down for maintenance. Pair with
Retry-Afterwhen possible. - 504 Gateway Timeout — an upstream service didn't respond in time.
Returning 500 for a timeout to a downstream API instead of 502/504 hides useful diagnostic information from anyone debugging the failure later, including future you.
A decision shortcut
Ask two questions in order: "Is this the client's fault?" — if yes, pick from 400/401/403/404/409/422/429 based on which specific thing is wrong. "Did something downstream fail?" — if yes, pick 502/503/504 over a generic 500 so the failure mode is visible in the status code itself, not just in a log line nobody's looking at yet.
Try it
HTTP Status Codes is a searchable reference for every standard status code with a plain-language explanation of when to use it — useful for the 2am moment when you're deciding between 409 and 422 and can't remember which one actually fits.
Hanuman Singh · built snaptxt.app · hanumansingh.dev