Skip to main content

HTTP Status Codes and Semantics

TL;DR
  • 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​

ANALOGYEveryday

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.

ANALOGYFor engineers

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:

ClassMeaningWho is at faultClient should
1xxInformational, an interim reply before the final onenobodykeep waiting for the final status
2xxThe request was received, understood and acceptednobodyuse the result
3xxFurther action needed, usually follow a Locationnobodyfollow it (bounded number of times)
4xxThe request is wrong or cannot be honoured as sentclientfix it; never resend unchanged
5xxA valid request failed on the server sideserver or something behind itretry 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).

CodeNameTypical useRetry?
100ContinueServer accepts the headers; client may send the body (after Expect: 100-continue)not an error
101Switching ProtocolsAccepting an Upgrade, e.g. HTTP to WebSocketnot an error
200OKSuccess with a body (GET) or a result (POST)not an error
201CreatedA POST or PUT created a resource; send Location pointing at itnot an error
202AcceptedQueued for later processing, not done yet; give a status URLnot an error
204No ContentSuccess, deliberately no body (typical for DELETE and PUT)not an error
206Partial ContentAnswer to a Range requestnot an error
301Moved PermanentlyThe URL changed for good; update bookmarks and linksfollow Location
302FoundTemporary move; historically ambiguous about the methodfollow Location
303See OtherResult lives elsewhere, fetch it with GET (POST then redirect)follow Location
304Not ModifiedYour cached copy is still valid; no bodyuse the cache
307Temporary RedirectTemporary move, method and body must be keptfollow Location
308Permanent RedirectPermanent move, method and body must be keptfollow Location
400Bad RequestMalformed syntax, invalid framing, unparseable bodyno, fix the request
401UnauthorizedNo or bad credentials; must carry WWW-Authenticateafter fixing credentials
403ForbiddenAuthenticated but not allowedno
404Not FoundNo such resource (the host did resolve and answer)no
405Method Not AllowedResource exists, this method does not; must carry Allowno
409ConflictClashes with current state: duplicate, edit conflict, version mismatchafter re-reading state
410GoneRemoved on purpose and not coming backno
413Content Too LargeBody exceeds what the server will acceptno, send less
415Unsupported Media TypeRequest Content-Type or Content-Encoding not understoodno, change format
422Unprocessable ContentSyntax fine, semantics invalid (validation failures)no, fix the data
429Too Many RequestsYou exceeded your quota; may carry Retry-Afteryes, after the wait
500Internal Server ErrorUnhandled failure in the servermaybe, idempotent only
501Not ImplementedServer does not support this method at allno
502Bad GatewayA gateway got an invalid response from upstreamyes, idempotent only
503Service UnavailableOverloaded or down for maintenance; may carry Retry-Afteryes, after the wait
504Gateway TimeoutA gateway did not get a response from upstream in timeyes, 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.

CodePermanent?Method on the follow-upCached by default?
301yesmay change POST to GET (historical behaviour)yes (heuristically)
302nomay change POST to GET (historical behaviour)only with explicit freshness headers
303noalways GET (HEAD stays HEAD)only with explicit freshness headers
307nomust not changeonly with explicit freshness headers
308yesmust not changeyes (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.

clientexample.comwww.example.comport 80 then 443port 443GET http://example.com/a301 Location: https://example.com/aGET https://example.com/a302 Location: https://www.example.com/aGET https://www.example.com/a200 OK2 RTT3 RTT3 RTTTotal before the page: 2 + 3 + 3 = 8 round tripsPOST on 301 or 302: client may resend as GET. POST on 307 or 308: same method and body.
One URL, two redirects, three requests, three connections. Each arrow is one request or response; slanted arrows show time passing. The final line shows why POST needs 307 or 308 if the body must survive.

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?​

Malformed or invalid request?Caller not identified?Identified but not permitted?Target does not exist?Clashes with current state?Caller over its quota?Failure on our side?400, or 422 if the syntax is fine401 + WWW-Authenticate403404, or 410 if removed on purpose409429 + Retry-After500, 502, 503 or 504yesyesyesyesyesyesyesnononononono
Walk top to bottom and stop at the first question you answer yes. The order matters: the client-side questions come first, server-side failures are what is left over.

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.
ClientCDN or LBAppDatabasereceives the status502 bad reply from app503 no healthy target504 app too slow500 unhandled bug503 shedding loadno HTTP codesapp maps failure to 500 or 503the LB writes these itselfwritten by your code
The load balancer or proxy sits between the client and your app and generates three different 5xx codes depending on what the app did. Your app emits 500 itself, and 503 when it sheds load. The database speaks SQL, not HTTP.

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.

MethodSafe (read-only)?Idempotent?Why
GET, HEAD, OPTIONS, TRACEyesyesread-only by definition
PUTnoyesreplace the resource with this exact state; repeating leaves the same state
DELETEnoyesafter the first, the resource is gone; repeating changes nothing
POSTnonoeach call may create another record or charge another card
PATCHnonot guaranteedidempotent 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 Accept headers 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/xml sent to a JSON-only endpoint), or has a Content-Encoding it 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:

{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"status": 403,
"detail": "Your balance is 30, but the item costs 50.",
"instance": "/account/12345/msgs/abc"
}

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 saysProduction doesWhy
A REST API uses every verb and a precise status code for every outcomeMany APIs use only GET and POST, or return 200 for everything and put {"error": ...} in the bodyClients 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 linksJSON over HTTP with URLs hard-coded in client code and documentationHypermedia 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 methodsSome 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 unauthorisedSome APIs return 403 for both, or 404 to hide that a resource existsSecrecy and simplicity beat precision; but clients lose the signal to re-authenticate
Retry any 5xxRetry only idempotent requests, only on a subset of 5xx, with backoff and a budgetA 5xx on a POST may mean the work already happened, and blind retries amplify an outage
A status code is the server's decisionA 502, 503 or 504 is usually written by a proxy, load balancer or CDN, not your codeGateways 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_ARGUMENT is 400, UNAUTHENTICATED 401, PERMISSION_DENIED 403, NOT_FOUND 404, RESOURCE_EXHAUSTED 429, UNAVAILABLE 503 and DEADLINE_EXCEEDED 504 (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-remaining and x-ratelimit-reset headers, 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 errors array 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​

HISTORY
  • 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+json as 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-Key header (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):

curl -sI -L http://github.com | grep -iE '^(HTTP|location)'
HTTP/1.1 301 Moved Permanently
Location: https://github.com/
HTTP/2 200

-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:

curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://github.com
301 https://github.com/

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):

curl -sIL https://httpbin.org/redirect/3 | grep -iE '^(HTTP|location)'
HTTP/2 302
location: /relative-redirect/2
HTTP/2 302
location: /relative-redirect/1
HTTP/2 302
location: /get
HTTP/2 200

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):

curl -s -D - -o /dev/null -r 0-99 https://httpbin.org/range/1000
HTTP/2 206
content-length: 100
accept-ranges: bytes
content-range: bytes 0-99/1000

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:

curl -s -o /dev/null -w '%{http_code}\n' https://httpbin.org/status/429
curl -sI -H 'Accept-Encoding: gzip' https://example.com | grep -iE '^(HTTP|content-encoding)'
429
HTTP/2 200
content-encoding: gzip

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​

GOTCHAS5
301 and 302 both keep POST
401 and 403 are interchangeable
5xx means retry forever
404 means the server does not exist
200 with an error body is fine

Interview questions​

INTERVIEWWhat is the difference between 401 and 403?junior
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 as Bearer or Basic.
  • 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.
INTERVIEWExplain 301, 302, 303, 307 and 308.junior
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.
INTERVIEWWhat do 2xx, 3xx, 4xx and 5xx mean, and who is at fault?junior
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.
INTERVIEWWhat is the difference between 502, 503 and 504, and where does each come from?mid
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_timeout both 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.
INTERVIEWWhat is idempotency, and which HTTP methods are idempotent? How do you make a POST payment retry-safe?mid
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.
INTERVIEWWhat does a good REST API look like, and where does REST break down?mid
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.
INTERVIEWHow should a client behave when it gets a 429?mid
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.
INTERVIEWShould validation errors be a 200 with an error body or a 4xx? Why does it matter?senior
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​

FACTSStatus code trivia
  • 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​

NUMBERSNumbers and rules to remember
  • 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