HTTP Status Codes Explained

An HTTP status code is the three-digit number a server sends with every response to say what happened to the request. The first digit gives the class — success, redirect, client error, server error — and the full code gives the specific reason. Reading them correctly is the fastest way to diagnose an API problem, and choosing them correctly is one of the clearest signs of a well-designed API.

This guide explains the five classes, the codes you meet daily, the difference between look-alikes (401 vs 403, 301 vs 302, 502 vs 504), and which code your own API should return in each situation.

The five classes

  • 1xx Informational — the request was received and processing continues (100 Continue, 101 Switching Protocols). Rarely seen by application code.
  • 2xx Success — the request was understood and accepted.
  • 3xx Redirection — the client must do something else, usually follow a new URL.
  • 4xx Client error — the request is wrong: bad syntax, missing auth, no such resource. Resending it unchanged usually gets the same answer; the exceptions are stated by the code itself, such as 429 (retry later, after the Retry-After time when one is sent) and 408 (the request may be repeated).
  • 5xx Server error — the request was valid but the server failed. A retry with backoff can help when the failure is temporary (502, 503, 504), but repeat automatically only idempotent requests such as GET, PUT or DELETE; a 501 will not change on a retry.

Try it: HTTP Status Code Lookup

2xx: success codes

  • 200 OK — the standard success; the body contains the result.
  • 201 Created — a resource was created (after POST or PUT); the Location header points at it.
  • 202 Accepted — the request was queued for asynchronous processing; not done yet.
  • 204 No Content — success with no body (typical for DELETE and some PUT/PATCH).
  • 206 Partial Content — a byte range was returned, used for resumable downloads and video.

3xx: redirects

  • 301 Moved Permanently — the URL has changed for good; browsers and search engines update their records. Use for canonical URL changes.
  • 302 Found — temporary redirect; the original URL stays the canonical one. The method may change to GET.
  • 303 See Other — after a POST, go and GET this other URL (the post/redirect/get pattern).
  • 304 Not Modified — the cached copy is still valid; no body is sent. A sign that caching works.
  • 307 / 308 — temporary and permanent redirects that guarantee the method and body are preserved. Prefer 308 over 301 for APIs.

Try it: Redirect Checker

4xx: client errors

  • 400 Bad Request — malformed syntax or invalid parameters; the general validation error.
  • 401 Unauthorized — no valid credentials. Despite the name it means "not authenticated". Send a WWW-Authenticate header.
  • 403 Forbidden — authenticated, but not allowed to do this. Retrying with the same credentials will not help.
  • 404 Not Found — no resource at this URL (or the server prefers not to say whether it exists).
  • 405 Method Not Allowed — the URL exists but not for this method (e.g. DELETE on a read-only resource). Include an Allow header.
  • 409 Conflict — the request clashes with current state: duplicate unique key, stale version, already processed.
  • 410 Gone — existed once, removed on purpose, will not return.
  • 415 Unsupported Media Type — wrong Content-Type on the request body.
  • 422 Unprocessable Content (formerly Unprocessable Entity) — the syntax is fine but the values fail validation. Widely used for field-level errors.
  • 429 Too Many Requests — rate limit hit; the Retry-After header says when to try again.

Try it: API Status Code Simulator Try it: HTTP Status Analyzer

5xx: server errors

  • 500 Internal Server Error — an unhandled exception. Never the correct answer to bad input; it means validation is missing.
  • 501 Not Implemented — the server does not support the method at all.
  • 502 Bad Gateway — a proxy or load balancer got an invalid response from the upstream server (it crashed, or sent garbage).
  • 503 Service Unavailable — the server is overloaded or in maintenance; temporary. Send Retry-After.
  • 504 Gateway Timeout — the proxy waited for the upstream and it never answered in time.
502 and 504 come from a proxy or load balancer, reporting that its upstream sent an invalid response or none in time; 500 and 503 describe the server itself.
  • 500 Internal Server Error

    • Client
    • Server
    1. 1 From Client To Server: Request
    2. 2 Server internally: Unhandled exception
    3. 3 From Server To Client: 500 reply
  • 502 Bad Gateway

    • Client
    • Proxy or load balancer
    • Upstream server
    1. 1 From Client To Proxy or load balancer: Request
    2. 2 From Proxy or load balancer To Upstream server: Forwards the request
    3. 3 From Upstream server To Proxy or load balancer: Invalid response reply It crashed, or sent garbage
    4. 4 From Proxy or load balancer To Client: 502 reply

    Outcome: The proxy answers: the upstream returned junk.

  • 503 Service Unavailable

    • Client
    • Server
    1. 1 From Client To Server: Request
    2. 2 Server internally: Overloaded or in maintenance
    3. 3 From Server To Client: 503 reply

    Outcome: Temporary: the server should send a Retry-After header.

  • 504 Gateway Timeout

    • Client
    • Proxy or load balancer
    • Upstream server
    1. 1 From Client To Proxy or load balancer: Request
    2. 2 From Proxy or load balancer To Upstream server: Forwards the request
    3. 3 Proxy or load balancer internally: Waits: no answer in time
    4. 4 From Proxy or load balancer To Client: 504 reply

    Outcome: The proxy answers: the upstream was too slow.

When the failure is temporary (502, 503, 504), a retry with backoff can help, but repeat automatically only idempotent requests such as GET, PUT or DELETE.

Try it: Mock HTTP Status Generator

Codes people confuse

  • 401 vs 403 — 401: who are you? (log in). 403: I know who you are, and the answer is no.
  • 301 vs 302 vs 308 — permanent vs temporary; 308 also keeps POST as POST.
  • 400 vs 422 — 400 for unparseable or structurally wrong requests, 422 for well-formed requests with invalid values. Pick one convention and keep it.
  • 502 vs 503 vs 504 — upstream returned junk vs server unavailable vs upstream too slow.
  • 404 vs 410 — unknown vs deliberately removed.

Which code should your API return?

Return the most specific code that is true, use it consistently, and always send a JSON error body with a stable machine-readable code and a human-readable message. Never wrap a failure in 200 — clients and monitoring both depend on the status line. Rate-limit with 429 and Retry-After, validate with 400/422, authenticate with 401, authorize with 403, and reserve 500 for bugs you intend to fix.

Try it: API Request Builder Try it: API Status Code Simulator

Frequently asked questions

What does HTTP status 200 mean?

OK — the request succeeded and the response body carries the result. It is the default success code for GET.

What is the difference between 401 and 403?

401 means the request has no valid authentication (log in or send a token). 403 means the identity is known but that action is not permitted.

What does a 502 Bad Gateway mean?

A proxy or load balancer in front of the application received an invalid response from it, or could not get a response at all — usually the application server crashed, restarted or dropped the connection. When the upstream is only too slow to answer, the proxy returns 504 instead.

What does 429 Too Many Requests mean?

The client has exceeded a rate limit. Wait for the time in the Retry-After header before sending more requests.

Should validation errors return 400 or 422?

Either is acceptable; 400 is the classic choice, 422 is common in modern APIs for field-level validation. Consistency across the API matters more than the choice.

Is 304 Not Modified an error?

No. It means the client's cached copy is still current, so the server sent no body. It saves bandwidth and is a sign that caching headers are working.

References

Practice exercise

Review the status codes of an online store’s Orders API

Your task

You test the Orders API of an online grocery store (fictional). Build 2.6.0 is on the staging environment. The material below contains the team’s API conventions, which are the test basis for status codes, and eleven requests from your test log with the responses you received. The mobile app developer has also sent you a question.

For every response, decide whether its status code and headers are right, and write the expected result when they are not. Then answer the developer and choose the finding you would report first. Pen and paper are enough; allow about 30 minutes. The HTTP Status Code Lookup gives the meaning of any code. If you want to turn your expected results into a repeatable check, write one response from the log out as a raw response and paste it into the API Response Validator: it checks the response against the status you expect and the headers that must be present, such as Location, WWW-Authenticate or Retry-After.

Material
Online grocery store (fictional) - Orders API, build 2.6.0 on the staging environment
Base URL: https://staging.grocery.example/api

Team API conventions (the test basis for status codes)
  C1  Create: 201 Created, with a Location header that holds the new order’s URL.
  C2  Validation: 400 when the body cannot be parsed; 422 when it parses but a value
      is invalid. Every error has a JSON body {"error":{"code":"...","message":"..."}}.
  C3  Auth: 401 with "WWW-Authenticate: Bearer" when the token is missing or expired;
      403 when the token is valid but the action is not allowed.
  C4  Privacy: another customer’s order is answered exactly like an unknown order (404).
  C5  State: an action that the order’s current state does not allow gets 409.
  C6  Rate limit: 60 requests per minute per token, then 429 with Retry-After.
  C7  Versioning: /v1 paths redirect permanently to /v2; the method and body are kept.
  C8  A failure is never returned with 200.

Test log - customer A’s token unless stated otherwise; bodies shortened
  R1   POST /v2/orders   valid basket, 3 items
       -> 200 OK   {"id":58213,"status":"PLACED",...}   no Location header
  R2   POST /v2/orders   "quantity": 0 on one item (the API spec requires 1 or more)
       -> 500 Internal Server Error   {"error":{"code":"INTERNAL",...}}
  R3   POST /v2/orders   body cut off after 40 bytes (invalid JSON)
       -> 400 Bad Request   {"error":{"code":"MALFORMED_JSON",...}}
  R4   GET /v2/orders/58213   no Authorization header
       -> 403 Forbidden   no WWW-Authenticate header
  R5   GET /v2/orders/58101   an order of customer B
       -> 404 Not Found   same headers and body as for the unknown order 99999999
  R6   POST /v2/orders/58090/cancel   the order has already shipped
       -> 200 OK   {"status":"error","message":"Order already shipped"}
  R7   GET /v2/orders?page=2   the 61st request within one minute
       -> 429 Too Many Requests   no Retry-After header
  R8   POST /v1/orders   valid basket (the path older app versions use)
       -> 301 Moved Permanently   Location: /api/v2/orders
       the client follows with GET /v2/orders -> 200 OK (order list); no order created
  R9   GET /v2/orders/58213   If-None-Match: "a7f3" (ETag of the app’s cached copy)
       -> 304 Not Modified   no body; the order has not changed since it was cached
  R10  GET /v2/orders/58213   at 14:02, during a deployment to staging
       -> 502 Bad Gateway   HTML error page from the load balancer
  R11  GET /v2/orders?from=2019-01-01&to=2026-09-30   customer A’s full history
       -> 504 Gateway Timeout after 30 s   HTML error page from the load balancer

Question from the mobile app developer
  "Our HTTP client automatically retries every 5xx response, up to 3 times.
   Is that safe for these requests?"
Steps
  1. For each row, write the status code and the headers the conventions expect before you look at the actual response.
  2. Compare with the actual response and mark the row correct, defect or question for the team. For a defect, name the convention (C1–C8) or the rule from the guide that it breaks.
  3. For R10 and R11, say what the gateway code tells you about where the problem is, and what you would record and retest.
  4. Answer the developer’s question for each request that got a 5xx response: may the app retry it automatically, and why? Then say whether any other response in the log allows a retry, and when.
  5. Pick the finding you would report first, write the title of its defect report and give the reason for the priority.
Show the model answer
Model answer

Summary: six defects in status codes or headers (R1, R2, R4, R6, R7, R8), three correct responses (R3, R5, R9), and two gateway codes that are correct but point at a problem behind the gateway (R10 to investigate, R11 a defect). The details follow row by row; each expected result comes from the conventions.

R1 — defect (C1). Expected result: 201 Created with Location: /api/v2/orders/58213. A 200 only says "it worked"; it does not say that a new resource exists, and a client that checks for 201, or reads the Location header to open the new order, cannot rely on this response.

R2 — defect (C2). The body parses and 0 breaks the rule of 1 or more, so the expected result is 422 with a JSON error that names the field quantity. The 500 is the failure; the defect behind it is missing validation: the value reached code that could not handle it, and a 500 is never the correct answer to bad input. Because 0 and 1 are the two boundary values of this rule, check that 1 is accepted, and also try -1, 1.5, "two" and a missing quantity to see how far the missing validation goes.

R3 — correct (C2). A body that cannot be parsed gets 400 with the JSON error body. A 422 would be wrong here, because nothing could be read to validate.

R4 — defect (C3). No token means "not authenticated": expected result 401 Unauthorized with WWW-Authenticate: Bearer, and RFC 9110 requires that header on every 401. A 403 tells the app "you are signed in but not allowed", so instead of sending the customer to the sign-in screen it shows a permission error. Retest with an expired token too, because C3 covers both cases, and keep one check that 403 still appears where it belongs: a valid token used for an action it is not allowed to perform.

R5 — correct (C4). The order exists but belongs to customer B. The team chose 404 so that the API does not reveal which order numbers exist, a use of 404 the guide describes (the server prefers not to say whether the resource exists). A 403 here would be a defect against C4, because it would confirm that order 58101 exists. The check that matters is the comparison: status, headers and body must be identical to those for the unknown order 99999999, and they are.

R6 — defect (C5 and C8). Expected result: 409 Conflict with the JSON error body, because cancelling clashes with the order’s current state (already shipped). A 200 with "status":"error" hides the failure: the app may tell the customer that the order was cancelled, and monitoring counts the call as a success. Also check the side effects: the order must still be in the shipped state, and no refund may have started.

R7 — defect (C6). 429 is the right code for the 61st request, but the Retry-After header that C6 requires is missing, so the app cannot know when to try again: it either sends more requests that get 429 or waits longer than it needs to. Check the boundary as well: the 60th request in the same minute must succeed.

R8 — defect (C7). Expected result: 308 Permanent Redirect with Location: /api/v2/orders. On a 301 a client may change POST to GET (RFC 9110 allows this for historical reasons), and that is exactly what happened: the basket never reached /v2, no order was created, and the last response in the chain is a 200, so the app may report success. The guide’s advice is to prefer 308 over 301 for APIs. Ask which app versions still call /v1: the answer decides how urgent this is.

R9 — correct. 304 is not an error: the order has not changed since the app cached it with ETag "a7f3", so the server sends no body and the app shows its cached copy. Test the opposite case as well: after the order changes (for example after a cancellation), the same request must return 200 with the new body and a new ETag; otherwise customers see a stale status.

R10 — not a defect in the status code; a question to investigate. 502 means the load balancer received an invalid response, or no response, from the application, which fits an application server restarting during the 14:02 deployment. Record the time, the build and the request, and retest after the deployment. If 502 appears outside deployments, it points to the application crashing or dropping connections: report it with the application logs. Two questions for the team: should the load balancer stop sending requests to a server before it restarts during a deployment, so that clients never see 502? And can the app handle an HTML error page where C2 promises a JSON body?

R11 — the 504 itself is correct: the load balancer waited 30 seconds and the application did not answer in time. The defect is behind it: a search over a long history (2019 to 2026) does not finish within the 30 seconds the gateway allows. Narrow the date range to find roughly where it starts to fail, record the response times, and report it as a performance defect. Ask the product owner whether there is a maximum date range: if there is, a range the API will not serve should be rejected with 422 (C2) instead of timing out. Repeating the same request will not help.

The developer’s question: not as it stands. Retry automatically only when both are true: the code is temporary (502, 503, 504) and the method is idempotent (GET, PUT, DELETE). R2 must not be retried, for two reasons. Its 500 is caused by the request itself, so it comes back the same every time. And POST is not idempotent, which is the bigger risk: if POST /v2/orders gets a 502 or 504, the server may already have created the order before the response was lost, and an automatic retry can create a second one. After a failed POST the app should check the order list with a GET before it offers to send again, and the developers should explain how the API prevents duplicate orders. R10 is safe to retry (GET, temporary code, with backoff). R11 is a GET too, but repeating a request that is too heavy only adds load and times out again, so keep the number of retries small and use backoff. Finally, 429 is a 4xx that may be retried, but only after the Retry-After time, which is one more reason R7 matters.

Report R8 first. Title, for example: "POST /v1/orders answers 301 instead of 308; the client resends it as GET and no order is created". It comes first because customers on any app that still calls /v1 lose their orders without any error message, and the chain ends with a 200. Then R6 (a failed cancellation shown as a success) and R4 (a missing token reported as a permission problem, so the app never sends the customer to sign in). If you put R6 first because you know that every app has moved to /v2, that is a sound reason; what matters is that the priority comes from the impact on customers. After the fixes, run the same rows again as confirmation tests.

To check your work: every row should have the expected code and, where the conventions ask for one, the header (Location, WWW-Authenticate, Retry-After). If you marked R3, R5 or R9 as a defect, reread what the guide says about 400 vs 422, about 404 (the server may prefer not to say whether a resource exists) and about 304 (not an error).

Your progress on this topic

Part of the QA Roadmap

Tools mentioned in this guide

Analyze the HTTP status code and headers of a response and get an explanation of whether it is correct for the situation.

Validation

Get a public endpoint that returns any HTTP status code, with optional delay and custom JSON body, to test client error handling.

API Testing

Trace redirect chains (301/302/307/308) and see every hop, status and final URL.

Web

Compose and send HTTP requests (method, URL, headers, auth, body) and inspect the response — a lightweight Postman in your browser.

API Testing

Test CORS configuration: send preflight and simple requests from a given origin and inspect the Access-Control-* headers.

Web

How Mutqan Soft can help

More guides

More guides