HTTP Status Codes and Semantics
- The first digit is the blame: 2xx done, 3xx go elsewhere, 4xx the client's request is wrong (do not resend it unchanged), 5xx the server (or something behind it) failed a valid request (may be retried, carefully).
- 301/308 are permanent, 302/307 are temporary. 301 and 302 let a client turn a POST into a GET; 307 and 308 must keep the method and body. 303 says "now GET this other URL".
- 401 means "I do not know who you are" (and must send
WWW-Authenticate); 403 means "I know who you are, and no". - 502 is a bad reply from upstream, 503 is "unavailable right now" (send
Retry-After), 504 is "upstream took too long". They are usually emitted by the proxy or load balancer, not your app. - Idempotent (GET, HEAD, PUT, DELETE, OPTIONS, TRACE) means repeating it has the same effect as doing it once, so it is safe to retry. POST is not; make it retry-safe with an
Idempotency-Key.
Mental model
A status code is the reply slip from a help desk. A slip stamped "done" (2xx) means your request was handled. "Go to window 7 instead" (3xx) comes with the new window number. "Your form is wrong" (4xx) means resubmitting the same form will get the same answer. "Our system broke" (5xx) means the form was fine and you may try again later.
Where it breaks: a human clerk never stamps "done" on a form they rejected. Servers do exactly that all the time, returning 200 OK with an error message in the body, and nothing in the protocol stops them.
Status codes are an exception hierarchy that crosses the network. 4xx is ValueError or PermissionError: the caller broke the contract, and retrying the same call is pointless. 5xx is RuntimeError: the callee failed, and a retry may succeed. 3xx is a return value that is a pointer, not data.
Where it breaks: an exception is raised by your callee. A 5xx often is not: the load balancer or CDN in front of your app invents 502, 503 and 504 on its own. And a timeout carries no information about whether the work happened, which an exception from a local call does.
How it works
Every HTTP response starts with a status line: version, a three-digit code, and a reason phrase (HTTP/1.1 404 Not Found). The message layout is covered in HTTP basics; this page is about what the three digits promise. The code is the only part a machine should branch on: the reason phrase is optional decoration and HTTP/2 and HTTP/3 drop it entirely.
The five classes are defined in RFC 9110, section 15:
| Class | Meaning | Who is at fault | Client should |
|---|---|---|---|
| 1xx | Informational, an interim reply before the final one | nobody | keep waiting for the final status |
| 2xx | The request was received, understood and accepted | nobody | use the result |
| 3xx | Further action needed, usually follow a Location | nobody | follow it (bounded number of times) |
| 4xx | The request is wrong or cannot be honoured as sent | client | fix it; never resend unchanged |
| 5xx | A valid request failed on the server side | server or something behind it | retry if safe, with backoff |
The codes interviewers expect
The last column answers "may a client retry this?". It assumes the request was idempotent; a non-idempotent request needs an idempotency key first (see Idempotency and safe retries).
| Code | Name | Typical use | Retry? |
|---|---|---|---|
| 100 | Continue | Server accepts the headers; client may send the body (after Expect: 100-continue) | not an error |
| 101 | Switching Protocols | Accepting an Upgrade, e.g. HTTP to WebSocket | not an error |
| 200 | OK | Success with a body (GET) or a result (POST) | not an error |
| 201 | Created | A POST or PUT created a resource; send Location pointing at it | not an error |
| 202 | Accepted | Queued for later processing, not done yet; give a status URL | not an error |
| 204 | No Content | Success, deliberately no body (typical for DELETE and PUT) | not an error |
| 206 | Partial Content | Answer to a Range request | not an error |
| 301 | Moved Permanently | The URL changed for good; update bookmarks and links | follow Location |
| 302 | Found | Temporary move; historically ambiguous about the method | follow Location |
| 303 | See Other | Result lives elsewhere, fetch it with GET (POST then redirect) | follow Location |
| 304 | Not Modified | Your cached copy is still valid; no body | use the cache |
| 307 | Temporary Redirect | Temporary move, method and body must be kept | follow Location |
| 308 | Permanent Redirect | Permanent move, method and body must be kept | follow Location |
| 400 | Bad Request | Malformed syntax, invalid framing, unparseable body | no, fix the request |
| 401 | Unauthorized | No or bad credentials; must carry WWW-Authenticate | after fixing credentials |
| 403 | Forbidden | Authenticated but not allowed | no |
| 404 | Not Found | No such resource (the host did resolve and answer) | no |
| 405 | Method Not Allowed | Resource exists, this method does not; must carry Allow | no |
| 409 | Conflict | Clashes with current state: duplicate, edit conflict, version mismatch | after re-reading state |
| 410 | Gone | Removed on purpose and not coming back | no |
| 413 | Content Too Large | Body exceeds what the server will accept | no, send less |
| 415 | Unsupported Media Type | Request Content-Type or Content-Encoding not understood | no, change format |
| 422 | Unprocessable Content | Syntax fine, semantics invalid (validation failures) | no, fix the data |
| 429 | Too Many Requests | You exceeded your quota; may carry Retry-After | yes, after the wait |
| 500 | Internal Server Error | Unhandled failure in the server | maybe, idempotent only |
| 501 | Not Implemented | Server does not support this method at all | no |
| 502 | Bad Gateway | A gateway got an invalid response from upstream | yes, idempotent only |
| 503 | Service Unavailable | Overloaded or down for maintenance; may carry Retry-After | yes, after the wait |
| 504 | Gateway Timeout | A gateway did not get a response from upstream in time | yes, idempotent only; work may have run |
Redirects: the five 3xx codes that move you
A 3xx tells the client to make another request, usually at the URL in the Location header. They differ on two axes that matter: will the client keep the method and body, and may the answer be remembered.
| Code | Permanent? | Method on the follow-up | Cached by default? |
|---|---|---|---|
| 301 | yes | may change POST to GET (historical behaviour) | yes (heuristically) |
| 302 | no | may change POST to GET (historical behaviour) | only with explicit freshness headers |
| 303 | no | always GET (HEAD stays HEAD) | only with explicit freshness headers |
| 307 | no | must not change | only with explicit freshness headers |
| 308 | yes | must not change | yes (heuristically) |
"Cached by default" comes from the list of heuristically cacheable codes in RFC 9110, section 15.1, which contains 301 and 308 but not 302, 303 or 307. The method rule for 301 and 302 is in section 15.4: a client "MAY" rewrite POST to GET, and in practice browsers do. The Fetch Standard, which browsers follow, spells out the same: 301 and 302 on a POST become GET, 303 becomes GET for every method but HEAD, and 307 and 308 keep the method.
Worked cost, assuming a 50 ms round trip and ignoring DNS: hop one is a TCP handshake (1 RTT) plus the request (1 RTT) = 2 RTT. Hops two and three each need a new TCP connection (1 RTT), a TLS 1.3 handshake (1 RTT, RFC 8446) and the request (1 RTT) = 3 RTT. Total 8 RTT = 400 ms, against 3 RTT = 150 ms if the client had gone straight to https://www.example.com/a. Fixing the links, or HSTS (the browser rewrites http:// to https:// before sending anything, saving the first hop), saves 100 ms with HSTS (the first hop is gone) or 250 ms with a direct final URL (all redirect hops are gone). Chrome's DevTools shows an HSTS rewrite as a synthetic 307 Internal Redirect. See the URL journey for where that sits in a page load.
Which code do I send?
Everything that falls through the last "no" is a success: 2xx, or a 3xx the client should follow.
Deep dive
401 versus 403
401 Unauthorized really means unauthenticated: the request lacked valid credentials. RFC 9110, section 15.5.2 requires the response to carry a WWW-Authenticate header naming the scheme (Basic, Bearer, and so on) so the client knows how to authenticate. Sending new credentials can turn a 401 into a 200. 403 Forbidden means the server understood who you are (or does not care) and refuses; repeating the request with the same identity changes nothing. The cookie, session and token mechanics behind this live on the cookies, sessions and CORS page (not written yet); this page only needs the distinction.
A common production variant: return 404 instead of 403 for resources the caller has no right to know about, so the response does not confirm they exist. That is a deliberate trade of correctness for secrecy; say so if you do it.
404, 410 and "no"
404 says "I have nothing at this URL" and says nothing about the future. 410 Gone says the removal is deliberate and permanent. Both, along with 405 and 414, are in the heuristically cacheable list, so a 404 can be cached by a CDN if no freshness header forbids it. A deploy that briefly 404s a path can keep serving 404 from an edge cache after the origin is fixed. 404 also never means the server does not exist: a name that does not resolve fails at DNS, and an unreachable host fails at TCP, long before any status code is produced.
Where 502, 503 and 504 come from
These three are the gateway family. A gateway (reverse proxy, load balancer, CDN, API gateway) forwards your request to an upstream and, when the upstream misbehaves, the gateway itself invents the status:
- 502 Bad Gateway: the upstream answered with something invalid, or closed the connection before replying. The app crashed mid-request, or spoke garbage.
- 503 Service Unavailable: temporarily not accepting work. Either the gateway has no healthy upstream to send to, or the app is deliberately shedding load. May carry
Retry-After(RFC 9110, section 10.2.3, either a number of seconds or an HTTP date). - 504 Gateway Timeout: the gateway waited for the upstream and gave up.
Concrete defaults, all documented: an AWS Application Load Balancer returns 502 when a target closes the connection or sends a malformed response, 503 when there are no registered or healthy targets, and 504 when the target does not respond before the idle timeout, 60 seconds by default (ALB troubleshooting, connection idle timeout). nginx as a reverse proxy returns 504 when the upstream does not answer within proxy_read_timeout, 60 seconds by default (ngx_http_proxy_module). That is why a request that "hangs for exactly a minute" and then fails with 504 is a timeout configuration clue before it is a bug clue. Load-balancer behaviour in depth is on load balancing and proxies and gateways.
429 and Retry-After
429 Too Many Requests (RFC 6585, section 4) means this caller exceeded its own limit; 503 means the server is overloaded for everybody. Both may carry Retry-After, which a well-behaved client honours before retrying, adding jitter so a thousand clients released at the same second do not return together. How servers decide and shed is on rate limiting and load shedding; how clients back off and cap retries is on timeouts, retries and backoff. This page does not repeat either.
Idempotency and safe retries
An operation is idempotent if doing it N times has the same effect on the server as doing it once (RFC 9110, section 9.2.2). The effect, not the response: a second DELETE may return 404 where the first returned 204, and it is still idempotent.
| Method | Safe (read-only)? | Idempotent? | Why |
|---|---|---|---|
| GET, HEAD, OPTIONS, TRACE | yes | yes | read-only by definition |
| PUT | no | yes | replace the resource with this exact state; repeating leaves the same state |
| DELETE | no | yes | after the first, the resource is gone; repeating changes nothing |
| POST | no | no | each call may create another record or charge another card |
| PATCH | no | not guaranteed | idempotent only if the patch is absolute (set x to 5), not relative (add 5 to x) |
The full methods list is in HTTP basics. Why the distinction matters is the lost reply. Say a client sends POST /payments and the connection dies. It cannot tell "never arrived" from "processed, reply lost". Retrying a non-idempotent call after three timeouts can run it up to three times: three charges.
The fix is an idempotency key. The client generates a unique value (a UUID) once per logical operation and sends it on every attempt, as the Idempotency-Key header. The server stores the key with the first outcome and, for the same key, returns the stored result instead of redoing the work. Stripe documents this on every POST: the key is up to 255 characters and Stripe prunes keys after at least 24 hours (Stripe idempotent requests). The header is also being standardised by the IETF HTTPAPI working group as draft-ietf-httpapi-idempotency-key-header; it was still a draft when this page was written, so check its datatracker page for the current state.
Two traps in the implementation: (1) the check-and-store must be atomic, or two concurrent retries both run; (2) if the same key arrives with a different body, reject it rather than silently replaying the old response. The wider policy (budgets, jitter, which errors to retry) is owned by timeouts, retries and backoff.
Content negotiation and compression
Content negotiation lets one URL have several representations. The client states preferences in request headers: Accept (media type), Accept-Language, Accept-Encoding (compression). The server picks one and says what it picked in the response (Content-Type, Content-Language, Content-Encoding) (RFC 9110, section 12). Two failures have their own codes:
- 406 Not Acceptable: the server cannot produce anything the
Acceptheaders allow. In practice servers usually ignore the preference and send a default instead of a 406, which RFC 9110 explicitly permits. - 415 Unsupported Media Type: the request body is in a format the server will not take (
Content-Type: text/xmlsent to a JSON-only endpoint), or has aContent-Encodingit cannot decode. 415 is the mirror image of 406.
Compression is negotiation applied to bytes. The client sends Accept-Encoding: gzip, br; the server compresses, sets Content-Encoding: gzip, and the receiver decodes before use. Content-Length then counts the compressed bytes. Since the response now depends on a request header, the server must add Vary: Accept-Encoding so a shared cache does not hand a gzip body to a client that cannot read it; the cache side of Vary is explained on HTTP caching. One security note: compressing responses that mix secrets with attacker-influenced text over TLS is the setup for the BREACH attack, so session tokens and CSRF tokens are the usual things people exclude or mask.
Range requests and 206
A client can ask for part of a resource: Range: bytes=0-99. A server that supports it answers 206 Partial Content with a Content-Range header and just those bytes; it advertises support with Accept-Ranges: bytes. A range outside the resource gets 416 Range Not Satisfiable. This is what makes download resume and video seeking work: the client asks for "the rest from byte N" instead of starting over. If the server ignores Range it answers 200 with the whole body, which is why clients check the status before assuming they received a slice. If-Range makes the request conditional, so a resume against a file that changed gets the new whole file instead of a corrupt splice. The spec is RFC 9110, section 14.
304 and 1xx
304 Not Modified answers a conditional request (If-None-Match, If-Modified-Since) and has no body by definition: the point is to save bytes. The validators and revalidation flow are owned by HTTP caching. 100 Continue lets a client send headers with Expect: 100-continue and wait for the server to approve before uploading a large body, so a request that would be rejected with 401 or 413 does not waste the upload.
REST in one section
REST is an architectural style from Roy Fielding's 2000 dissertation, not a protocol. Its constraints (Fielding, chapter 5) are client-server, stateless (every request carries all the context; the server keeps no session between requests), cacheable, uniform interface, layered system, and optional code on demand. The uniform interface itself has four parts: resources identified by URLs, manipulated through representations, self-descriptive messages, and hypermedia as the engine of application state (HATEOAS: responses contain the links that say what you can do next).
For everyday API design this boils down to: nouns in URLs (/orders/42, not /getOrder), verbs from the method (GET, POST, PUT, DELETE), status codes for outcomes, and statelessness so any server behind a load balancer can answer any request. The Richardson maturity model grades this in four levels: one endpoint (0), resources (1), proper methods and codes (2), hypermedia (3). Most "REST" APIs stop at level 2. Fielding has written that an API without hypermedia should not be called REST (REST APIs must be hypertext-driven); the industry ignored this and uses the word loosely. REST breaks down for action-shaped operations (POST /orders/42/cancel is a verb in a noun's clothing) and chatty or streaming workloads; the alternatives are compared on REST, gRPC, GraphQL and webhooks.
A standard error body
Choosing the code is half of a good error. RFC 9457 (2023, replacing RFC 7807) defines application/problem+json, a small JSON object with type, title, status, detail and instance, so clients can parse any compliant API's errors the same way:
The status member repeats the HTTP status for the benefit of tools that lose it (logs, queues); it is advisory and must match the real status line.
Textbook vs real world
| The textbook says | Production does | Why |
|---|---|---|
| A REST API uses every verb and a precise status code for every outcome | Many APIs use only GET and POST, or return 200 for everything and put {"error": ...} in the body | Clients and tooling are easier to write against one shape, and teams copy what they have seen. It breaks proxies, caches, retry logic and dashboards that only read the status line |
| REST means HATEOAS: the client follows links | JSON over HTTP with URLs hard-coded in client code and documentation | Hypermedia clients need generic link-following logic nobody wanted to build; OpenAPI documents give the same discoverability at design time |
| PUT and DELETE are first-class methods | Some clients and old middleboxes only pass GET and POST, so APIs accept POST with an X-HTTP-Method-Override header (a convention, not a standard) | Legacy proxies and firewalls that only understood the two original form methods |
| 401 means unauthenticated, 403 means unauthorised | Some APIs return 403 for both, or 404 to hide that a resource exists | Secrecy and simplicity beat precision; but clients lose the signal to re-authenticate |
| Retry any 5xx | Retry only idempotent requests, only on a subset of 5xx, with backoff and a budget | A 5xx on a POST may mean the work already happened, and blind retries amplify an outage |
| A status code is the server's decision | A 502, 503 or 504 is usually written by a proxy, load balancer or CDN, not your code | Gateways synthesise a response when the upstream does not give one |
In production
- Google APIs map each canonical RPC error to one HTTP code and a structured body:
INVALID_ARGUMENTis 400,UNAUTHENTICATED401,PERMISSION_DENIED403,NOT_FOUND404,RESOURCE_EXHAUSTED429,UNAVAILABLE503 andDEADLINE_EXCEEDED504 (Google API design guide: errors, AIP-193). - Stripe documents its HTTP codes (200, 400, 401, 402 for a valid request that failed, 403, 404, 409, 424, 429 and 5xx) together with machine-readable error types, and recommends idempotency keys for retrying POSTs (Stripe errors, idempotent requests).
- GitHub signals REST rate limiting with 403 or 429 and the
x-ratelimit-remainingandx-ratelimit-resetheaders, so a client has to check headers rather than the code alone (GitHub: rate limits for the REST API). - Cloudflare adds its own 5xx family when it cannot get a good answer from your origin: 520 (unknown error from the origin), 521 (origin refused the connection), 522 (connection to origin timed out), 523 (origin unreachable), 524 (origin took too long to reply), 525 and 526 (TLS problems with the origin). These are not in any RFC and they identify which hop failed, which is the first thing to read when debugging (Cloudflare 5xx errors).
- AWS load balancers generate 502, 503 and 504 for the reasons in the deep dive; the ALB docs list the specific causes for each (ALB troubleshooting).
- GraphQL servers have conventionally answered 200 with an
errorsarray even when the query failed; the GraphQL over HTTP specification is an effort to standardise which codes to use when, so check which response media type your server and client agree on.
History and evolution
- 1996 RFC 1945 documents HTTP/1.0 with 16 status codes (200, 201, 202, 204, 300, 301, 302, 304, 400, 401, 403, 404, 500, 501, 502, 503). The three-digit, first-digit-is-the-class design is already there.
- 1998 RFC 2324, an April Fools' joke, defines the Hyper Text Coffee Pot Control Protocol and 418 "I'm a teapot". RFC 9110 later marks 418 as reserved and unused so it is not reassigned.
- 1999 HTTP/1.1 (RFC 2616) adds the 1xx class and more codes, including 303, 307, 409 and 410. What broke: 302 was widely implemented as "temporary, and turn POST into GET", contrary to its original text, so 303 and 307 were added to name the two behaviours separately.
- 2000 Roy Fielding's dissertation defines REST. The industry adopts the name and, mostly, the resource-and-verbs half.
- 2012 RFC 6585 adds 429 Too Many Requests, 428, 431 and 511, because every large API was inventing its own rate-limit status.
- 2015 RFC 7538 adds 308 Permanent Redirect. What broke: there was a temporary method-preserving redirect (307) but no permanent one, so permanent moves of POST endpoints had to use 301 and risk a method change.
- 2016 RFC 7725 adds 451 Unavailable For Legal Reasons.
- 2022 RFC 9110 consolidates HTTP semantics into one document independent of the wire version, and renames a few codes (413 becomes Content Too Large, 422 Unprocessable Content).
- 2023 RFC 9457 standardises
application/problem+jsonas a shared error body. - Now the full list lives in the IANA HTTP Status Code Registry; codes keep behaving identically across HTTP/1.1, HTTP/2 and HTTP/3 because the semantics are separate from the framing.
- Next a standard
Idempotency-Keyheader (an IETF draft at time of writing) and wider use of problem+json for machine-readable errors.
Hands-on
Follow a redirect and see each Location. These runs used curl 8.7.1 on macOS; Linux curl prints the same status lines (output abbreviated):
-I sends HEAD, -L follows redirects. Look at the first line: the plain-HTTP request returned 301, then curl made a second, HTTPS request. Get just the code and the target without following:
Watch a chain with a limit (--max-redirs 0 stops at the first redirect; the curl default for -L is 50, browsers stop sooner, Fetch specifies 20):
Three 302s, then the 200. Note that Location can be a relative URL.
Ask for the first 100 bytes and read the 206 (the -r flag sets the Range header):
Compare: the 100-byte Content-Length is the slice, and 0-99/1000 says "bytes 0 to 99 of 1,000". Now force a status and a negotiated encoding:
The second shows negotiation working: ask for gzip, the response says it chose gzip. Without --compressed, curl will not decode a gzip body for you. Use curl -v to see the full request and response headers; the other curl switches are on curl and openssl. httpbin.org is a third-party test service and is sometimes slow or down; any local server works the same way.
Gotchas
Interview questions
Show answer
401 means the request is unauthenticated: no credentials or bad ones. The server should send a WWW-Authenticate header saying how to authenticate, and fresh credentials can change the outcome. 403 means the server knows who you are and refuses, so retrying with the same identity will not help.
Follow-ups:
- What header goes with 401?
WWW-Authenticate, naming the scheme such asBearerorBasic. - Why would an API return 404 instead of 403? To avoid confirming that a resource the caller may not see exists.
- Where does the logged-in state come from? Cookies or tokens; that mechanism is on the cookies and sessions page.
Show answer
301 and 308 are permanent, so clients and search engines can update their links and caches may remember them. 302 and 307 are temporary. The other axis is the method: 301 and 302 allow a client to switch POST to GET, which browsers do, while 307 and 308 require the method and body to stay the same. 303 explicitly says "fetch the result with GET".
Follow-ups:
- What does SEO care about? Permanent redirects (301, 308) signal that the new URL is the canonical one; Google documents how it treats permanent and temporary redirects differently.
- When do I pick 307 over 302? When a POST or PUT must be replayed unchanged to the new URL.
- What stops infinite loops? Clients cap redirects: Fetch specifies 20, curl defaults to 50.
Show answer
2xx is success, 3xx tells the client to go elsewhere, 4xx means the client's request is wrong, and 5xx means the server failed a valid request. The class tells you who must change something: for 4xx the caller, for 5xx the operator. A 4xx should not be retried unchanged; a 5xx may be.
Follow-ups:
- Which classes are retryable? Mostly 5xx and 429, and only when the request is idempotent or protected by an idempotency key.
- Is 408 a client or server error? It is 4xx: the client was too slow to send the request.
- What if a server misuses the classes? Then proxies, caches and retries all misbehave, which is why status hygiene matters.
Show answer
All three are usually produced by a gateway: a load balancer, reverse proxy or CDN. 502 means the upstream sent an invalid reply or closed the connection. 503 means nothing healthy is available or the service is deliberately shedding load, and it can carry Retry-After. 504 means the gateway timed out waiting for the upstream. Your app itself emits 500 for its own bugs and 503 when it sheds.
Follow-ups:
- A request fails with 504 after exactly 60 seconds. What do you suspect? A proxy or load-balancer timeout; the ALB idle timeout and nginx
proxy_read_timeoutboth default to 60 seconds. - Is it safe to retry a 504? Only if idempotent: the upstream may have finished the work after the gateway gave up.
- How do you tell which hop produced it? Response headers (
Server,Via, a CDN's ray id) and gateway access logs.
Show answer
An operation is idempotent if repeating it leaves the server in the same state as doing it once. GET, HEAD, OPTIONS, TRACE, PUT and DELETE are idempotent; POST and usually PATCH are not. For a payment POST the client generates a unique Idempotency-Key per logical payment and sends it on every attempt; the server stores the key with the first result and returns that same result for repeats instead of charging again.
Follow-ups:
- Is DELETE idempotent if the second call returns 404? Yes; idempotency is about server state, not the response code.
- What can go wrong in the server implementation? A non-atomic check-and-store lets two concurrent retries both run; and reusing a key with a different body must be rejected.
- How long do you keep keys? Long enough to cover client retry windows; Stripe keeps them at least 24 hours.
Show answer
Resources are nouns with stable URLs (/orders/42), the method carries the verb, status codes carry the outcome, and requests are stateless so any server can answer any call. It is cacheable where it should be, and errors use a consistent body such as problem+json. REST strains on action-shaped operations like "cancel this order", on very chatty clients that need many round trips, and on streaming, where RPC or GraphQL fit better.
Follow-ups:
- What is HATEOAS? Responses include the links to the next valid actions; most real APIs skip it.
- How do you model an action? Either a sub-resource (
POST /orders/42/cancellation) or a state change via PATCH, accepting that it is a pragmatic compromise. - Why does statelessness help scale? No server-side session means a load balancer can send any request to any instance.
Show answer
Stop sending at that rate. Read Retry-After (seconds or an HTTP date) and wait at least that long; if it is absent use exponential backoff. Add random jitter so many clients do not retry in lock-step, and cap the total number of retries. Log it, because a persistent 429 means the client's design or quota is wrong.
Follow-ups:
- Why jitter? Without it every client released by the same limiter window comes back at the same instant and re-triggers the limit.
- 429 or 503? 429 means this caller exceeded its quota; 503 means the server is overloaded for everyone.
- Is the 429 itself retry-safe for POST? The request was rejected before running, but use an idempotency key anyway in case an earlier attempt succeeded.
Show answer
A 4xx (400 or 422) with a problem-style body. Intermediaries and tools only read the status line: caches decide what to store from it, retry logic and circuit breakers key off it, load balancers and monitors count it, and clients branch on it. A 200 that carries an error tells all of them "success", so failures never trigger alerts or retries and never appear in error-rate metrics.
Follow-ups:
- What does a 200-with-error do to your SLO dashboard? If the SLI is "non-5xx responses over all responses", a 2 percent failure rate hidden in 200 bodies reads as 100 percent available while 2 in every 100 users fail.
- When is an in-body error legitimate? Partial success in a batch endpoint: the overall call worked, with per-item errors (some APIs use 207 or 200 with per-item results).
- 400 or 422? 400 for malformed syntax or framing, 422 for well-formed data that fails validation; many teams pick one and apply it consistently.
Fun facts
- 418 I'm a teapot comes from RFC 2324, an April Fools' joke from 1998. RFC 9110 reserves the code and marks it unused so real implementations cannot reassign it.
- 451 Unavailable For Legal Reasons (RFC 7725) is a nod to Ray Bradbury's novel Fahrenheit 451.
- There is no 306. RFC 2616 lists 306 as "(Unused)": the code was used in an earlier draft, is reserved, and has never been given a meaning.
- A 404 can be cached, and so can 405, 410 and 501, because RFC 9110 lists them as heuristically cacheable.
- The reason phrase does not exist in HTTP/2 or HTTP/3, which carry only the numeric status.
Cheat sheet
- 5 classes - 1xx info, 2xx success, 3xx redirect, 4xx client, 5xx server
- 16 - status codes in HTTP/1.0 (RFC 1945, 1996)
- 301 / 308 - permanent; 302 / 307 - temporary; 307 / 308 - keep the method
- 60 s - default ALB idle timeout and nginx
proxy_read_timeout(a common 504 clock) - 20 / 50 - redirect cap in the Fetch spec / default for
curl -L - 8 RTT - cost of http to https 301 then www 302 on 50 ms links: 400 ms before the page starts
- GET, HEAD, PUT, DELETE, OPTIONS, TRACE - idempotent; POST - not