Reference Cards
Reference Cards

HTTP status codes: which one, and why

Lesson · ~25 min
After this unit you can
  • Choose 200, 201 or 204 for a success, and 301, 302, 303, 307 or 308 for a redirect by asking: is it permanent, and may the method change?
  • Tell 401 from 403, 400 from 422 and 415, and 404 from 410 by the symptom
  • Pick 409 for a state conflict and 429 for rate limiting, and tell 502, 503 and 504 apart by which hop failed
  • Recall the header each code relies on: Location, WWW-Authenticate, Allow, Retry-After, and the If-None-Match behind a 304

Cards

200, 201, 204 — pick a 2xx by what you send back

Flag
CodeSend it whenBody
200 OKthe response carries the result: a GET's resource, an action's outcomeyes
201 Createdthe request made a new resource; point at it with Locationusually the new resource
204 No Contentit worked and there's nothing to add: a save, a DELETEnone, by definition

RFC 9110 says a 204 "cannot contain content". Node's http server drops a body you write with one, and a client that always parses JSON throws on it:

const res = await fetch("/api/cart/items/9", { method: "DELETE" }); // 204
await res.json(); // SyntaxError: Unexpected end of JSON input

Check res.status === 204 before parsing.

301, 302, 307, 308 — permanent or temporary, method kept or not

Flag

All four send the new URL in Location. Two questions pick the code:

POST may become GETmethod and body kept
permanent (update your links)301308
temporary (keep using the old URL)302307

The left column is history: browsers came to re-send a redirected POST as a GET, and RFC 9110 now allows it. The Fetch standard, which browsers and Node's fetch follow, does exactly that — after a 301 or 302, a POST becomes a body-less GET (other methods are kept). 307 and 308 exist to promise the method survives.

HTTP/1.1 308 Permanent Redirect
Location: /v2/uploads

For a GET, 301 and 308 behave alike, as do 302 and 307. For anything else, choose from the right column — or 303 when you want a GET (next card).

303 See Other — Post/Redirect/Get

Flag

After a form POST succeeds, answer 303 with Location set to the result page. The browser follows with a GET, so the address bar shows /orders/42 and a refresh reloads that page instead of re-submitting the order.

HTTP/1.1 303 See Other
Location: /orders/42

303 means "the answer is over there — go GET it". The Fetch standard turns every method except GET and HEAD into GET on a 303 (PUT and DELETE too), where 302 only converts POST. That's also why 307 is wrong here: it would re-POST the form, body and all, to /orders/42.

301 and 308 stick — permanent redirects are cacheable by default

Flag

RFC 9110 makes 301 and 308 heuristically cacheable: with no Cache-Control or Expires, a browser or CDN may store the redirect and follow it again without asking your server. Ship a wrong permanent redirect and clients that cached it keep going to the wrong place — the old URL is never requested, so your fix can't reach them until their copy expires or is cleared.

  • Trying out a new redirect? Start with 302 or 307; switch to permanent once it's right.
  • Want a permanent redirect you can still change? Give it an explicit lifetime:
HTTP/1.1 301 Moved Permanently
Location: https://example.com/docs/
Cache-Control: max-age=3600

302 and 307 are not heuristically cacheable: a client stores them only when you add caching headers.

304 Not Modified — "use the copy you already have"

Flag

A 304 answers a conditional GET or HEAD: the client holds a cached copy and asks for the resource only if it changed. If it hasn't, the server sends headers and no body, and the client reuses its copy.

GET /app.js HTTP/1.1
If-None-Match: "v42"

HTTP/1.1 304 Not Modified
ETag: "v42"

The validator came from an earlier 200: its ETag goes back in If-None-Match, its Last-Modified in If-Modified-Since. A 304 is not an error, and the 304s in the DevTools network panel are the cache working. When the browser's own cache made the request conditional, fetch hands your code the stored 200, not the 304.

400 vs 422 — can't read it, or read it and it's wrong

Flag
  • 400 Bad Request — the server can't or won't process the request as sent: malformed JSON, broken framing. The broad client-error fallback.
  • 422 Unprocessable Content — the content type is understood (so not 415) and the syntax is correct, but the instructions can't be carried out. Valid JSON that breaks a rule:
{ "start": "2026-05-10", "end": "2026-05-01" }

422 came from WebDAV; RFC 9110 (2022) moved it into core HTTP and renamed it from "Unprocessable Entity". Both codes are used for validation errors — NestJS's ValidationPipe answers 400 by default, plenty of APIs use 422 — so pick one per API and keep it.

What's never right: letting a bad body surface as 500 because JSON.parse threw and nothing caught it.

401 vs 403 — "who are you?" vs "no"

Flag
  • 401 Unauthorized — the request lacks valid credentials: no token, an expired one, a bad signature. Despite the name it means unauthenticated. The server must send WWW-Authenticate naming a scheme the client can use, and retrying with new credentials can succeed.
  • 403 Forbidden — the server understood the request and refuses. If credentials were sent, they're valid but not enough: a signed-in non-admin calling an admin route. Re-sending the same credentials won't help, so a client shouldn't retry automatically.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer

Client rule of thumb: on 401, refresh the token or send the user to sign in; on 403, show "you don't have access" and keep the session.

404 vs 410 — and 404 as a quiet 403

Flag
  • 404 Not Found — nothing here now. It says nothing about whether that's temporary or permanent.
  • 410 Gone — it was here, it was removed on purpose, and it isn't coming back. It asks crawlers and link-checkers to drop their links. Use it only when you know the removal is permanent; otherwise 404.

RFC 9110 also lets a server answer 404 instead of 403 to hide that a resource exists. Answering 404 for another tenant's /invoices/981, exactly as for an id that doesn't exist, stops a caller from probing which ids are real.

Both codes are heuristically cacheable: without Cache-Control, a 404 for something created a minute later may still be served from a cache.

405 Method Not Allowed — and the Allow header it must carry

Flag

The URL exists, but not for this method: a POST /users/42 where only GET, PUT and DELETE are handled. RFC 9110 says a 405 must list what the resource does support:

HTTP/1.1 405 Method Not Allowed
Allow: GET, PUT, DELETE

Its neighbours:

  • 404 Not Found — there's no such resource at all.
  • 501 Not Implemented — the server doesn't recognise the method for any resource (a made-up BREW, say).

409 Conflict — fine on its own, clashes with the current state

Flag

The request is well-formed, but it conflicts with the resource as it is now, and the client may be able to fix that and resubmit. Two everyday cases:

  • Uniqueness: POST /users with an email that's already registered.
  • Lost update: a PUT carrying "version": 7 when the stored record is at version 8 because someone else saved first.

RFC 9110 asks for enough detail to see what clashed:

{ "error": "version_conflict", "yourVersion": 7, "currentVersion": 8 }

If the version travels in an If-Match header instead of the body, the precise code is 412 Precondition Failed. And versus 422: a 422 body is wrong in itself; a 409 body would succeed against a different state.

415 Unsupported Media Type — the body's format, not its contents

Flag

The server won't take the body in the format it was sent: its Content-Type (or Content-Encoding) isn't one this endpoint accepts. The classic cause: curl -d labels its data application/x-www-form-urlencoded, even when the data is JSON.

curl -d '{"name":"Ada"}' https://api.example.com/users      # sent as a form
curl --json '{"name":"Ada"}' https://api.example.com/users  # sent as JSON (curl 7.82+)

Keep it apart from its neighbours:

  • 406 Not Acceptable is about what the client asked to get back (Accept); 415 is about what it sent (Content-Type).
  • 422 is the step after: format understood, contents wrong.

429 Too Many Requests — rate limited, with Retry-After

Flag

Defined in RFC 6585, not RFC 9110: this client sent too many requests in some window. The server decides how it counts — per user, per API key, per IP — and may say when to come back:

HTTP/1.1 429 Too Many Requests
Retry-After: 120

Retry-After is either a number of seconds or an HTTP date. A well-behaved client waits at least that long; a retry loop that ignores it just keeps hitting the limit. Caches must not store a 429.

Its neighbours: 503 is about the server, not this client's rate — it's temporarily unable to handle the request (overload, maintenance); 403 means the request is refused however slowly you send it.

500 Internal Server Error — the server's bug, never the client's

Flag

A 500 means an unexpected condition stopped the server — typically an unhandled exception. It's the generic 5xx: use a more specific code when you know one (503 for overload, a 4xx when the client's input is the problem).

Two habits:

  • Don't let bad input become a 500. A missing field that crashes body.name.trim() is a client error you didn't check for — validate it and answer 400 or 422.
  • Don't send the stack trace. Log it with a request id and return a generic body:
{ "error": "internal_error", "requestId": "req_8f2c" }

5xx rates usually drive alerts and error budgets, so a client mistake reported as 500 pages your on-call engineer instead of telling the caller what to fix.

502 vs 503 vs 504 — which hop failed

Flag

With a proxy, load balancer or CDN in front of your app, read these as where it broke:

CodeUsually sent byWhat happened
502 Bad Gatewaythe proxyit got an invalid response from the app — garbage, or the connection closed before the headers came (a crash)
503 Service Unavailablethe app or the proxytemporarily can't serve: overload, maintenance; may send Retry-After
504 Gateway Timeoutthe proxythe app didn't answer in time — the proxy's timeout fired first (a slow query, a hung handler)

When the app does send a proper error response, the proxy should pass it through, not replace it with a 502.

Not 408 Request Timeout: that's a server giving up on a client that didn't finish sending its request.

Quiz

GET /me arrives with an expired access token. Which response?

Flag

A signed-in user with a valid token calls DELETE /admin/users/7, but only admins may delete users. Which response?

Flag

DELETE /cart/items/9 succeeds. The client has already removed the row from its own state and needs nothing back. Which response fits best?

Flag

A summer promotion page has been taken down for good. There's no replacement, and you want search engines to drop their links to it. What should GET /promo/summer-2025 return?

Flag

A request body arrives as {"name": "Ada", — the JSON is cut off. Which response?

Flag

A JSON-only endpoint that checks Content-Type first receives curl -d '{"name":"Ada"}' https://api.example.com/users (no -H). Which response?

Flag

POST /bookings sends Content-Type: application/json and the well-formed body {"start":"2026-05-10","end":"2026-05-01"}. Which code describes the problem most precisely?

Flag

Two editors open document 12 at version: 7. The first saves, so it's now at version 8. The second sends PUT /docs/12 with "version": 7 in the body. Which response?

Flag

Your API allows 100 requests per minute per API key. One key sends its 101st request inside the minute. Which response?

Flag

A checkout form POSTs to /checkout. On success the browser should land on /orders/42, and refreshing that page must not place the order again. Which redirect?

Flag

POST /v1/uploads is moving permanently to /v2/uploads. Old clients that follow the redirect must still upload their file there. Which code?

Flag

A reverse proxy with a 60-second upstream timeout sits in front of your Node API. A report query hangs, and after 60 seconds the client gets an error from the proxy. Which code?

Flag

Fill in the blank

POST /orders created order 42. Which header tells the client where the new order lives?

Flag
HTTP/1.1 201 Created
: /orders/42

A 401 must name the authentication scheme the client should use.

Flag
HTTP/1.1 401 Unauthorized
: Bearer

A 405 must list the methods the resource does support.

Flag
HTTP/1.1 405 Method Not Allowed
: GET, PUT, DELETE

Tell a rate-limited client to wait two minutes before trying again.

Flag
HTTP/1.1 429 Too Many Requests
: 

The browser has /app.js cached with ETag: "v42" and revalidates it. Fill in the request header that carries the ETag, and the status code the server answers with when the file hasn't changed.

Flag
GET /app.js HTTP/1.1
: "v42"

HTTP/1.1 

Further reading