HTTP Status Codes Guide: 2xx, 3xx, 4xx, 5xx and How to Respond
An API integration passes every test case and fails on its first production run, returning 418 from a service that never heard of a teapot. A client's retry logic turns one upstream blip into a self-inflicted outage. And a working redirect now bounces users between two login pages — one character in the status code is the whole difference.
Status codes are a shared vocabulary, and almost every mishandling comes from treating the number as arbitrary rather than a contract. It says what happened, whether to retry, and whose problem it is.
The problem: a status code is an instruction, not a label
The status code is not a description of your server's mood. It is a machine-readable decision about what the caller should do next. The classes form a hierarchy of responsibility:
| Class | Meaning | Whose problem | Retry? |
|---|---|---|---|
2xx | Success | Nobody | N/A |
3xx | Redirection | Client, if it follows | Follow the Location |
4xx | Client error — request was wrong | Caller | No — fix the request |
5xx | Server error | Your service | Yes, with backoff |
The habit people skip: 4xx means do not retry unchanged, 5xx means you may. A client retrying a 400 can never succeed and keeps the load up; one treating a 503 as fatal turns a blip into an outage.
Within the classes, codes are more specific than assumed:
200 OKversus201 Created— a POST that created something should return201with aLocationheader.204is success, no body.400 Bad Request— malformed: invalid JSON, missing field.422— valid syntax, rejected content.401 Unauthorized— despite the name, unauthenticated: credentials missing or invalid, and it should carry aWWW-Authenticateheader.403 Forbidden— authenticated but not permitted. Retrying cannot help. Hence404is correct when you hide a resource's existence — a403confirms it exists.409 Conflict— conflicts with current state: a duplicate key, a lost edit race.
The 401/403 distinction is routinely collapsed, producing tickets reading "I'm not allowed" when the real problem is an expired token.
The solution: 301 and 302 are not interchangeable
The two redirect codes that cause real damage differ in one respect: whether the method survives.
301 Moved Permanently | 302 Found | |
|---|---|---|
| Meaning | Moved, forever | Elsewhere, for now |
| Caches | Cache aggressively, permanently | Do not cache |
| Method after redirect | Historically POST → GET | Preserved |
| Typical use | http → https, moved domain | Login redirects, A/B tests |
Three consequences follow, each of which has caused a real outage.
Cache permanence. A 301 tells every cache between you and the client to stop asking. Send one during a migration then change your mind, and caches, proxies and CDNs keep going to the old location for hours.
Method rewriting. Historically a user agent following a 301 after a POST reissued it as GET, dropping the body. 307 (and 308) guarantee method and body survive — essential when a login form posts and the response redirects. 301 also transfers ranking signal, so using it on a temporary page is a slow SEO wound.
The rule: if you would be surprised to see the redirect cached forever, it is not a 301. For http → https it is right.
429 and 503: the two codes with operational contracts
Both mean "come back later" and say when to return.
429 Too Many Requests means the client exceeded a limit the server enforces — a rate limit, a quota, a concurrency ceiling. The critical half is the Retry-After header. Without it every client invents its own backoff, badly — retrying at once, which keeps you rate-limited.
503 Service Unavailable means the server cannot handle the request now — an overloaded instance, a dependency down, a deploy in progress. It is the right code for a load balancer shedding traffic or a tripped breaker.
Both codes carry the same header, and its two legal formats differ in practice. Delta-seconds (Retry-After: 30) is what nearly every implementation emits, because the client can apply it without knowing the time. HTTP-date (Retry-After: Wed, 08 Oct 2026 12:00:00 GMT) names an instant, so it survives a long pause or a queued request — but it depends on clock agreement, and skew turns it into a wait that is too short or too long. Emit delta-seconds if you emit only one; parse the date form with a real HTTP-date parser rather than splitting on the comma.
Treat the value as a floor, not a target — which is why jitter is not optional. A rate limit gets hit by many clients at once, often because they share a cron schedule or deploy script. If every client obeys Retry-After: 30 to the millisecond, they all return at second 30 and immediately re-trip the limit: a synchronised retry wave that keeps the system saturated long after it could have recovered. Full jitter (random duration in [0, backoff)) spreads that wave out; equal jitter (roughly half the window) is a reasonable middle ground when you need a minimum delay. So "we implemented retry-after" and "we implemented correct backoff" are different claims.
This is also why a server shedding load should answer 503 with Retry-After rather than 429: 429 tells the client it did something wrong, and a client that believes it is being throttled for good behaviour will back off, give up, or route around you.
The distinction that matters: 429 blames the client, 503 blames the server. A 503 is a signal to your own on-call, a 429 to the client's backoff logic. Sending 503 when you mean "slow down" hides a client problem; sending 429 when your database is down invites callers to retry into a failing system.
Nearby: 502 Bad Gateway means an intermediary got an invalid upstream response, 504 Gateway Timeout that it gave up waiting.
Tool walkthrough: using status codes to debug a failing call
The fastest way to reason about a status code is to read the category, the header and the body together. The HTTP Status Codes reference lists every code with its class, official meaning and the client behaviour each commits to — so you can check what you return against what the spec implies.
When a call fails:
- Read the class first.
4xxmeans inspect the request before touching the server;5xxmeans inspect the server. This one step eliminates half the investigation. - Distinguish
401from403. On401, trace the credential path; on403, the credential is fine and the answer is permissions. - Check the headers, not just the code. On
429, readRetry-After; on401, readWWW-Authenticate; on3xx, readLocationand check the method. - Then read the body. Most frameworks return a structured error body whose inner code distinguishes what the status code cannot — commonly a
200that should have been a201, or a500returned for what was really bad input.
FAQ
What is the difference between 401 and 403?401 Unauthorized actually means unauthenticated — credentials missing, malformed or expired — and should carry a WWW-Authenticate header. 403 Forbidden means the identity is known but not allowed here, so retrying cannot help.
Should I use 301 or 302 for a temporary redirect? Use 302 for anything temporary. If the redirect follows a POST or any non-GET request, prefer 307, which guarantees method and body survive. Reserve 301 for genuinely permanent moves.
Why must a 429 response include Retry-After? Without it every client has to guess how long to wait, and clients that guess badly retry immediately — keeping the limit in force and prolonging the throttle. It turns the 429 from "you are limited" into "you are limited, and here is when to return".
More developer tools on DigDevBox
- HTTP Status Codes reference — every code, its class and the client behaviour it implies
- HTTP Headers Viewer — read the headers that carry the real signal
- HTTP Status Checker — confirm a live endpoint's actual response code
- JWT Decoder & Parser — find out why a request came back
401