How to buildOctober 12, 20268 min read
Developer ReferenceStep 3 of 8

HTTP status codes: a reference

In short

A status code is a three-digit number whose first digit sets the class: 1xx interim response, 2xx success, 3xx redirection, 4xx client error, 5xx server error. Per RFC 9110 a client must understand at least the class and treat an unknown code as the x00 of that class. APIs mostly need 200, 201, 204, 304, 400, 401, 403, 404, 409, 422, 429, 500 and 503.

ReferenceLevel: BeginnerHTTP Status Codes

After this you will be able to

  • state the exact meaning of common 1xx–5xx codes per RFC 9110 and RFC 6585
  • pick a status code for an API response and tell apart 401/403, 400/422, 404/410, 301/308
  • return errors as application/problem+json (RFC 9457) and check response headers with curl -i

Verified with

RFC 9110 (HTTP Semantics)RFC 6585RFC 9457 (Problem Details)Node.js 24 LTS (24.18, 24.20)curl 8.21, 8.22

Checked against docs: October 12, 2026

Contents
  1. 01Status code classes and general rules
  2. 021xx and 2xx: interim responses and success
  3. 033xx: redirects and 304
  4. 044xx: client errors
  5. 055xx: server errors
  6. 06Common mix-ups and an API cheat sheet
  7. 07Error bodies: Problem Details (RFC 9457)
  8. 08Check it: a node:http server and curl -i

Status code classes and general rules

1xx

Meaning per RFC 9110
Informational: request received, processing continues
What the client does
Wait for the final response; there may be several interim ones

2xx

Meaning per RFC 9110
Successful: request received, understood and accepted
What the client does
Use the response

3xx

Meaning per RFC 9110
Redirection: further action is needed
What the client does
Follow Location or use the cached copy (304)

4xx

Meaning per RFC 9110
Client error: bad request or it cannot be fulfilled
What the client does
Fix the request; repeating it unchanged will not help (except 408, 429)

5xx

Meaning per RFC 9110
Server error: a valid request was not fulfilled
What the client does
Retry later if the operation is idempotent
  • Valid codes are 100 to 599. A client that does not know a code treats it as the x00 of its class: an unfamiliar 471 is handled as 400.
  • The reason phrase (Not Found, Created) is only a recommendation and may be replaced or omitted. Software should look at the number.
  • A request gets zero or more 1xx responses followed by exactly one final response of another class.
  • Some codes are heuristically cacheable without explicit headers: 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501.

1xx and 2xx: interim responses and success

100 Continue

Meaning
The start of the request was received and not rejected
When to send it
The client sent Expect: 100-continue before a large body; usually handled by the server or framework

101 Switching Protocols

Meaning
The server switches to the protocol named in Upgrade
When to send it
For example, switching to WebSocket

103 Early Hints

Meaning
Hints about headers of the coming response (RFC 8297)
When to send it
Send Link: rel=preload while the page is still being built

200 OK

Meaning
The request succeeded; the content depends on the method
When to send it
GET with data, POST/PUT with a result in the body

201 Created

Meaning
A new resource was created; its URI is in Location or the request URI
When to send it
A POST that created a record; the body usually describes it

202 Accepted

Meaning
Accepted for processing but not finished — and it may never be
When to send it
Background jobs: return a URI where the status can be checked

204 No Content

Meaning
Success, with no body and none allowed
When to send it
DELETE, PUT/PATCH with nothing to return

206 Partial Content

Meaning
Part of the resource, per the Range header
When to send it
Resumable downloads, video seeking

3xx: redirects and 304

301 Moved Permanently

Meaning
The resource moved for good; references should be updated
When to send it
A page changed its URL; for POST a browser may switch to GET

302 Found

Meaning
Temporarily at another URI; the old URI stays canonical
When to send it
A temporary move for GET

303 See Other

Meaning
See another resource (GET its Location)
When to send it
After a form POST, to the result page (Post/Redirect/Get)

304 Not Modified

Meaning
Conditional GET/HEAD: the client's copy is still valid
When to send it
The request had If-None-Match / If-Modified-Since and nothing changed

307 Temporary Redirect

Meaning
Temporarily elsewhere; the method MUST NOT change
When to send it
A temporary API move where POST and its body must be kept

308 Permanent Redirect

Meaning
Permanently elsewhere; the method must not change
When to send it
A permanent API move: /v1/orders → /v2/orders

Permanent

Method may change to GET
301
Method and body preserved
308

Temporary

Method may change to GET
302
Method and body preserved
307
301, 302, 307, 308 — two axes of choice

4xx: client errors

400 Bad Request

Meaning
The server cannot or will not process the request due to a client error
When to send it
Broken JSON, bad syntax, invalid headers

401 Unauthorized

Meaning
No valid credentials; WWW-Authenticate is required
When to send it
No token, or the token is expired or invalid

403 Forbidden

Meaning
The request is understood but the server refuses it
When to send it
The user is known but lacks permission; retrying with the same credentials is pointless

404 Not Found

Meaning
No current representation — or the server will not disclose that one exists
When to send it
No such record; also instead of 403 to hide existence

405 Method Not Allowed

Meaning
The method is known but not supported by the resource; Allow is required
When to send it
PUT on a resource that only has GET and DELETE

406 Not Acceptable

Meaning
No representation matches the client's Accept
When to send it
The client asks for XML, the API only serves JSON

408 Request Timeout

Meaning
The server did not receive a complete request in time
When to send it
Usually sent by the server or proxy itself

409 Conflict

Meaning
Conflict with the resource's current state; the client may resolve it
When to send it
Duplicate unique value, concurrent modification

410 Gone

Meaning
The resource is gone, likely for good
When to send it
An ended promotion, a deleted account — when the server knows for sure

412 Precondition Failed

Meaning
A condition in the headers (If-Match etc.) evaluated to false
When to send it
Lost update protection: the ETag has changed

413 Content Too Large

Meaning
The body is larger than the server will accept
When to send it
An upload that is too big

415 Unsupported Media Type

Meaning
The body format is not supported
When to send it
Form data sent instead of application/json; Accept may list what works

422 Unprocessable Content

Meaning
Type and syntax are fine, but the content cannot be processed
When to send it
Validation errors: empty required field, a date in the past

428 Precondition Required

Meaning
The server requires a conditional request (RFC 6585)
When to send it
PUT without If-Match where lost updates matter

429 Too Many Requests

Meaning
Too many requests in a period (RFC 6585); Retry-After is allowed
When to send it
Rate limit exceeded; the response must not be cached

431 Request Header Fields Too Large

Meaning
Headers are too large — overall or a single one (RFC 6585)
When to send it
Bloated cookies

5xx: server errors

500 Internal Server Error

Meaning
An unexpected condition prevented fulfilling the request
When to send it
An unhandled exception; details go to the log, not the client

501 Not Implemented

Meaning
The server does not support the required functionality
When to send it
The method is unknown to the server for any resource

502 Bad Gateway

Meaning
A gateway or proxy got an invalid response from the server behind it
When to send it
Nginx could not reach the app or got garbage back

503 Service Unavailable

Meaning
Temporarily unable to handle the request: overload or maintenance; Retry-After allowed
When to send it
Planned maintenance, load shedding

504 Gateway Timeout

Meaning
A gateway or proxy did not get a timely response from the server behind it
When to send it
The app behind the proxy takes longer than the timeout

511 Network Authentication Required

Meaning
The client must authenticate to get network access (RFC 6585)
When to send it
Wi-Fi captive portals; not used in APIs

Common mix-ups and an API cheat sheet

401 vs 403

Difference
401 — the server does not know who you are (missing or invalid credentials); 403 — it knows and still says no
What to return
No token → 401 with WWW-Authenticate; wrong role → 403

400 vs 422

Difference
400 — the request cannot be parsed (broken JSON); 422 — parsed, but the data fails validation
What to return
Syntax → 400, business validation → 422

404 vs 410

Difference
404 says nothing about permanence; 410 — the server knows it is permanent
What to return
Not sure → 404

404 vs 403

Difference
404 may replace 403 to avoid revealing that a resource exists
What to return
Someone else's order by direct link → often 404

409 vs 412

Difference
409 — state conflict; 412 — an explicit header condition failed
What to return
If-Match mismatch → 412

301/302 vs 307/308

Difference
301/302 allow POST to become GET, 307/308 do not
What to return
For APIs — 308 or 307

500 vs 502/503/504

Difference
500 — the app itself failed; 502/504 — trouble between proxy and app; 503 — temporarily unavailable
What to return
Check who produced the response: the app or the proxy

200 with an error in the body

Difference
Clients, caches and monitoring see a success
What to return
Always send the real error code

GET /items/1

Success
200, 304 on a matching ETag
Typical errors
404, 401, 403

POST /items

Success
201 + Location; 202 for a background job
Typical errors
400, 409, 415, 422, 429

PUT/PATCH /items/1

Success
200 with the resource, or 204
Typical errors
404, 409, 412, 422

DELETE /items/1

Success
204
Typical errors
401, 403, 404

Unsupported method

Success
—
Typical errors
405 + Allow
Which code to return: typical REST API operations

Error bodies: Problem Details (RFC 9457)

The code says what happened; the body says why. RFC 9457 defines one JSON format with the application/problem+json media type, so a client parses errors from every endpoint the same way. All members are optional, and you can add extensions such as an errors list of fields.

type

Contents
URI of the problem type; when absent it is about:blank — “no meaning beyond the HTTP code”

title

Contents
Short summary of the type; for about:blank, the status phrase (Not Found)

status

Contents
The status code as a number — advisory only; the actual response must use the same code

detail

Contents
Explanation of this occurrence that helps fix the request; clients should not parse it

instance

Contents
URI of this specific occurrence
JSON
{
  "type": "about:blank",
  "title": "Unprocessable Content",
  "status": 422,
  "detail": "Field name is required",
  "errors": [
    {
      "pointer": "#/name",
      "detail": "must be a non-empty string"
    }
  ]
}
Body of the real 422 response from the server below, formatted for reading.

Check it: a node:http server and curl -i

This dependency-free server returns the key codes with the right headers. Run node server.mjs (Node.js 24; if port 3000 is taken, PORT=3100 node server.mjs) and use curl -si in a second terminal: -i prints the status line and headers, -s hides the progress meter.

server.mjs
import { createServer } from "node:http";

const PORT = Number(process.env.PORT ?? 3000);
const items = new Map([[1, { id: 1, name: "coffee" }]]);
let nextId = 2;
const ETAG = '"v1"';
const hits = [];

function sendJson(res, status, data, headers = {}) {
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", ...headers });
  res.end(JSON.stringify(data));
}

// RFC 9457: errors as application/problem+json; "status" must equal the real status code
function problem(res, status, title, extra = {}, headers = {}) {
  res.writeHead(status, { "Content-Type": "application/problem+json", ...headers });
  res.end(JSON.stringify({ type: "about:blank", title, status, ...extra }));
}

async function readBody(req) {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  return Buffer.concat(chunks).toString("utf8");
}

const server = createServer(async (req, res) => {
  const { pathname } = new URL(req.url, "http://localhost");

  try {
    if (pathname === "/old-page") {
      res.writeHead(301, { Location: "/new-page" });
      return res.end();
    }
    if (pathname === "/api/v1/orders") {
      res.writeHead(308, { Location: "/api/v2/orders" }); // method and body are preserved
      return res.end();
    }
    if (pathname === "/new-page" || pathname === "/api/v2/orders") {
      return sendJson(res, 200, { method: req.method, path: pathname });
    }
    if (pathname === "/promo-2024") return problem(res, 410, "Gone", { detail: "The promotion has ended" });
    if (pathname === "/maintenance") return problem(res, 503, "Service Unavailable", {}, { "Retry-After": "120" });
    if (pathname === "/crash") throw new Error("db connection lost");

    if (pathname === "/limited") {
      const now = Date.now();
      while (hits.length && now - hits[0] > 60_000) hits.shift();
      if (hits.length >= 2) return problem(res, 429, "Too Many Requests", {}, { "Retry-After": "60" });
      hits.push(now);
      return sendJson(res, 200, { ok: true });
    }

    if (pathname === "/items" && req.method === "POST") {
      if (!req.headers["content-type"]?.startsWith("application/json")) {
        return problem(res, 415, "Unsupported Media Type", {}, { Accept: "application/json" });
      }
      let data;
      try {
        data = JSON.parse(await readBody(req));
      } catch {
        return problem(res, 400, "Bad Request", { detail: "Request body is not valid JSON" });
      }
      if (typeof data?.name !== "string" || data.name.trim() === "") {
        return problem(res, 422, "Unprocessable Content", {
          detail: "Field name is required",
          errors: [{ pointer: "#/name", detail: "must be a non-empty string" }],
        });
      }
      if ([...items.values()].some((i) => i.name === data.name)) {
        return problem(res, 409, "Conflict", { detail: `Item "${data.name}" already exists` });
      }
      const item = { id: nextId++, name: data.name };
      items.set(item.id, item);
      return sendJson(res, 201, item, { Location: `/items/${item.id}` });
    }

    const match = pathname.match(/^\/items\/(\d+)$/);
    if (match) {
      const item = items.get(Number(match[1]));
      if (req.method === "GET") {
        if (!item) return problem(res, 404, "Not Found");
        const cache = { ETag: ETAG, "Cache-Control": "no-cache" };
        if (req.headers["if-none-match"] === ETAG) {
          res.writeHead(304, cache); // no body: the client reuses its cached copy
          return res.end();
        }
        return sendJson(res, 200, item, cache);
      }
      if (req.method === "DELETE") {
        const auth = req.headers.authorization;
        if (!auth) return problem(res, 401, "Unauthorized", {}, { "WWW-Authenticate": 'Bearer realm="api"' });
        if (auth !== "Bearer admin-token") return problem(res, 403, "Forbidden", { detail: "Admin role required" });
        if (!item) return problem(res, 404, "Not Found");
        items.delete(item.id);
        res.writeHead(204);
        return res.end();
      }
      return problem(res, 405, "Method Not Allowed", {}, { Allow: "GET, DELETE" });
    }

    return problem(res, 404, "Not Found");
  } catch (err) {
    console.error(err); // details go to the server log, not to the client
    return problem(res, 500, "Internal Server Error");
  }
});

server.listen(PORT, () => console.log(`API: http://localhost:${PORT}`));
A teaching example, not production: admin-token is a stub in place of real JWT or session checks, and the rate-limit counter is shared by all clients and kept in memory.
terminal
$ curl -si http://localhost:3000/items/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
ETag: "v1"
Cache-Control: no-cache
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"id":1,"name":"coffee"}

$ curl -si -H 'If-None-Match: "v1"' http://localhost:3000/items/1
HTTP/1.1 304 Not Modified
ETag: "v1"
Cache-Control: no-cache
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
The first request gets 200 and an ETag; the repeat with If-None-Match gets a body-less 304. Node.js 24.20 + curl 8.22, real output.
terminal
$ curl -si -X POST http://localhost:3000/items -H 'Content-Type: application/json' -d '{"name":"tea"}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /items/2
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"id":2,"name":"tea"}

$ curl -si -X POST http://localhost:3000/items -H 'Content-Type: application/json' -d '{"name":""}'
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Unprocessable Content","status":422,"detail":"Field name is required","errors":[{"pointer":"#/name","detail":"must be a non-empty string"}]}

$ curl -si -X DELETE http://localhost:3000/items/2
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
WWW-Authenticate: Bearer realm="api"
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Unauthorized","status":401}

$ curl -si -X PUT http://localhost:3000/items/1
HTTP/1.1 405 Method Not Allowed
Content-Type: application/problem+json
Allow: GET, DELETE
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Method Not Allowed","status":405}
201 with Location, 422 with Problem Details, 401 with WWW-Authenticate, 405 with Allow. Node.js still prints the old phrase Unprocessable Entity (from WebDAV, RFC 4918) while RFC 9110 names the code Unprocessable Content — only the number matters to clients.
terminal
$ curl -si http://localhost:3000/old-page
HTTP/1.1 301 Moved Permanently
Location: /new-page
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

$ curl -sL -d 'x=1' http://localhost:3000/old-page
{"method":"GET","path":"/new-page"}

$ curl -sL -d 'x=1' http://localhost:3000/api/v1/orders
{"method":"POST","path":"/api/v2/orders"}

$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/limited
200

$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/limited
200

$ curl -si http://localhost:3000/limited
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 60
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Too Many Requests","status":429}

$ curl -si http://localhost:3000/crash
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Internal Server Error","status":500}
On 301, curl with -L repeats the POST as a GET; on 308 it keeps the method. The third /limited request within a minute gets 429 and Retry-After; the 500 does not leak the exception text. Real output.

How requests, headers and JSON work is covered in “HTTP and JSON: requests, responses, headers and data format”; choosing codes, error formats and pagination for a whole API is in “API design: resources, versioning, errors, pagination”.

How this was verified2 · passedchecked 12 October 2026

“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.