Skip to content
shellhacks.dev
APIs

What Are HTTP Status Codes Developers Actually Meet

The HTTP status codes developers actually meet, grouped by meaning, with what scripts should do for each, retries, redirects and curl options.

6 min read
Summarize in AI
Illustration for "What Are HTTP Status Codes Developers Actually Meet": a terminal session, a request travelling to a server, an exit status badge.

The list of HTTP status codes has about sixty entries and a working developer meets fewer than twenty. Knowing those, and the right response in a script to each, covers most API failures. The exact semantics are set by the standard, and APIs sometimes bend them. The family tables here pair with the error posts 401 versus 403, 429 and backoff and 502 and 504. Examples use curl 8.

See the code in a script

Shell
curl -s -o /dev/null -w '%{http_code}\n' https://api.example.test/items
code=$(curl -s -o response.json -w '%{http_code}' https://api.example.test/items)
curl -sS -I https://api.example.test/items | head -1
curl -fsS https://api.example.test/missing; echo "exit: $?"
Text
200
HTTP/2 200
curl: (22) The requested URL returned error: 404
exit: 22

-w '%{http_code}' prints the status; without -f curl exits 0 on any HTTP response, which is why a script that does not check the code treats an error page as success. With -f (fail), curl exits 22 for status 400 and above; add --fail-with-body (curl 7.76 and later) to keep the response body for the error message.

2xx: it worked

Text
200 OK              request succeeded, body has the result
201 Created         a resource was created; look at the Location header
202 Accepted        queued, not finished; poll for the result
204 No Content      success, nothing to return (common for DELETE)
206 Partial Content a range of the resource (resumed downloads)

Treat the whole 2xx class as success but handle 202 specially: the work is not done yet, and the response usually points to a status URL. A 204 has an empty body, so piping it to jq fails with a parse error; check the code first.

3xx: go somewhere else

Text
301 Moved Permanently   update your URL; method may change to GET for POST
302 Found               temporary; browsers switch POST to GET
303 See Other           follow with GET
304 Not Modified        your cached copy is current (response to a conditional request)
307 Temporary Redirect  repeat the same method at the new URL
308 Permanent Redirect  permanent, same method

curl does not follow redirects unless you pass -L; a script that gets a 301 for an http:// URL that redirects to https:// and ignores it sees an empty or odd body. With -L, a POST redirected by 301 or 302 is re-sent as GET unless you add --post301, so prefer 307 and 308 in your own APIs. 304 is the answer to a conditional request, see avoiding repeated API calls.

4xx: the request is the problem

Text
400 Bad Request           malformed syntax or invalid parameters   fix the request
401 Unauthorized          no or invalid credentials                refresh or supply credentials
403 Forbidden             authenticated but not allowed            change permissions, not credentials
404 Not Found             no such resource (or hidden)             check the URL and ID
405 Method Not Allowed    wrong HTTP method                        check the Allow header
408 Request Timeout       the server gave up waiting               retry
409 Conflict              state conflict (duplicate, version)      resolve, then retry
410 Gone                  permanently removed                      stop asking
413 Payload Too Large     body exceeds the limit                   send less
415 Unsupported Media     wrong Content-Type                       set the header
422 Unprocessable Entity  well-formed but semantically invalid     read the error details
429 Too Many Requests     rate limited                             back off, honour Retry-After

A 4xx tells you that repeating the same request will fail again, so scripts should not retry them blindly. The exceptions are 408 and 429. Distinguishing 401 and 403 matters for what to do next, see 401 versus 403. A 404 may also be returned instead of 403 to avoid revealing that a resource exists. The details of error bodies are in parsing API errors.

5xx: the server failed

Text
500 Internal Server Error  unhandled failure in the application
501 Not Implemented        the server does not support the method
502 Bad Gateway            a proxy got an invalid response from the upstream
503 Service Unavailable    overloaded or in maintenance; may have Retry-After
504 Gateway Timeout        a proxy timed out waiting for the upstream

These are usually temporary, so retry with exponential backoff and a limit, but only for idempotent requests (GET, PUT, DELETE): a retried POST may have been processed the first time. 502 and 504 come from proxies and load balancers and point to the upstream being down or slow, as in 502 and 504 errors. Honour a Retry-After header when present.

A retry rule for scripts

Shell
case $code in
  2??) ok ;;
  408|429|500|502|503|504) retry_with_backoff ;;
  4??) fail "client error $code" ;;
  3??) follow_or_fail ;;
esac

This small case statement is a sound default. The full retry pattern, with jitter and a limit, is in rate limits and retries in scripts.

Reading a response as a whole

The status code is one line of information in a response that carries several. The headers add context: Content-Type says how to parse the body, Location says where a created or moved resource lives, Retry-After says how long to wait, WWW-Authenticate says what credentials are expected, and rate-limit headers say how much budget is left. A script that looks only at the code discards the most actionable information. Capture the headers with -D to a file and read the ones relevant to the status you got.

The body is the third part. Well-designed APIs return a structured error with a machine-readable code, a human-readable message and a request identifier. Log all three. Poorly designed ones return an HTML error page from a proxy in front of the application, which is a signal in itself: a 502 with an HTML body came from the proxy, a 502 with JSON came from the application, and the owner of the fix is different in each case. Check the Content-Type header before parsing the body as JSON, and treat a mismatch as an infrastructure failure.

Idempotency and what is safe to retry

Retry decisions depend on the method as much as on the code. GET, HEAD, PUT and DELETE are defined to be idempotent: sending them twice leaves the server in the same state as sending them once. POST is not, so retrying after a timeout may create a second order or send a second email. Many APIs offer an idempotency key header for this reason: the client sends a unique value with the request, and the server returns the stored result for a repeat. Generate the key once per logical operation, not per attempt, and keep it across retries.

A timeout is the hardest case because the status is unknown. The server may have processed the request and failed to answer. Treat unknown outcomes as questions to resolve, by reading the resource's state before retrying a non-idempotent call, not as failures to repeat blindly. A script that creates resources and retries on timeout without checking will, on a bad day, create them several times.

Using codes in your own API

If you write an API, the codes are part of its contract. Use 201 with a Location header for creation, 204 for success with no body, 400 for syntax and validation errors with details of which field failed, 404 for a missing resource, 409 for a conflict such as a duplicate, 422 for well-formed but semantically invalid input when you want to separate it from 400, and 429 with Retry-After for rate limits. Keep 500 for real bugs and alert on them, since a 500 means your code did something you did not expect. Returning 200 with an error field defeats every generic client, cache and monitor, because they all key on the code.

Monitoring by status class

Counting codes by class gives a fast health signal. A rise in 5xx means the service is failing, a rise in 4xx after a release usually means a client or a contract change, and a spike in 429 means somebody is exceeding limits. Chart the ratio of each class to total traffic instead of raw counts so the graph reads the same at night and at noon. Alert on the 5xx ratio crossing a threshold for several minutes, and look at 4xx trends weekly. Both are one line in most log tools, and they find problems before users report them.

Where this does not apply

Some APIs return 200 with an error in the body (GraphQL, some JSON-RPC and older SOAP-style services), so read the payload too. Others use nonstandard codes: 520 to 527 are Cloudflare's, 499 is nginx's "client closed request". The standard is RFC 9110. See the status codes section of RFC 9110 and the MDN status code reference.

Taggedhttp
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.