2.2
HTTP Methods and Status Codes
Safe, idempotent and cacheable methods, and what every status-code family tells the client.
50 min Reading + animations
Tip: tap or hover any dotted-underlined term to see what it means.
By the end of this lesson you will be able to
- Say exactly what every HTTP method means (GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, CONNECT, TRACE, and the new QUERY), and which ones carry a body.
- Classify each method as safe, idempotent and cacheable, and explain why each property matters to browsers, proxies, caches and retry logic.
- Choose between PUT and PATCH, and between JSON Merge Patch and JSON Patch; prevent lost updates with
If-Match. - Explain why retrying a POST after a timeout can charge a customer twice, and how an idempotency key fixes it.
- Read status codes as a senior engineer does: the five families, the codes that matter (100 to 504), which redirects keep the method, 401 vs 403, 404 vs 410, and who generated a 502, 503 or 504.
- Decide which responses are safe to retry, and design error responses with problem details (RFC 9457) instead of “200 with an error inside”.
#Where this lesson fits
Lesson 1.1 introduced the request–response cycle, a table of methods, the status codes you meet every day, and the rule “retry only idempotent requests”. This lesson takes those two tables apart. Every row hides a decision that some piece of software makes without understanding your application: a browser deciding whether it may prefetch a link, a proxy deciding whether it may resend a request to another server, a CDN deciding whether it may store a response, a client library deciding whether a failure is worth a retry. They decide from the method and the status code alone.
So the method and the status code are a contract with every machine on the path, not just with the person reading your API docs. Use them correctly and caching, retries and monitoring work for free. Use them loosely and you get double payments, cached error pages and dashboards that stay green during an outage.
Two other lessons build on this one: 2.3 covers the headers (including the CORS preflight that uses OPTIONS) and 2.4 covers caching and validation (ETags and 304) in depth. The HTTP version does not matter here: HTTP/1.1, HTTP/2 and HTTP/3 (lesson 2.1) carry exactly the same methods and status codes. Their meaning is defined once, in RFC 9110, HTTP Semantics (2022).
#The method: a promise about side effects
The first word of every request is the method (sometimes called the verb). It tells the server what the client wants done with the resource named by the URL. Methods are case-sensitive and written in capitals by convention.
| Method | What it asks for | Request body | Typical success |
|---|---|---|---|
| GET | Send me the current representation of this resource | No defined meaning | 200 (or 206 for a range, 304 if unchanged) |
| HEAD | Same as GET, but send only the headers | No | 200 |
| POST | Process this body according to the resource’s own rules | Yes | 201 + Location (created), 200 (result), 202 (queued), 204 |
| PUT | Make the resource at this URL exactly this (create or replace) | Yes, the full representation | 201 (created), 200 or 204 (replaced) |
| PATCH | Apply this set of changes to the resource | Yes, a patch document | 200 or 204 |
| DELETE | Remove the resource at this URL | No defined meaning | 204, 200, or 202 (deletion queued) |
| OPTIONS | What can I do with this resource? | Rarely | 204 or 200, with an Allow header |
| CONNECT | Proxy, open a raw tunnel to this host and port | No | 200, then raw bytes |
| TRACE | Echo my request back so I can see what arrived | No (must not have one) | 200, usually disabled |
#Every method in detail
GET fetches a representation. It is the most common request on the web and the only one most caches store. A GET may be conditional (If-None-Match, answered with 304 Not Modified, lesson 2.4) or a range request (Range: bytes=0-1023, answered with 206 Partial Content, used by video seeking and resumable downloads). The notes’ table says a GET has no body; precisely, a body on a GET is not forbidden but has no defined meaning: many servers, proxies and CDNs ignore or drop it, and some reject the request. Do not put data that matters in a GET body.
HEAD is GET without the response body. The headers are the ones a GET would return, including Content-Length, Last-Modified and ETag. It is used to check whether something exists, how big a file is before downloading it, or whether a link is still alive, without moving the bytes.
POST is the general-purpose method. The notes call it “create”, and creating an item in a collection (POST /orders) is the most common use, but RFC 9110 defines it more broadly: the target resource processes the enclosed body according to its own semantics. Submitting a form, appending a comment, starting a job (POST /reports), running an action (POST /orders/77/cancel) and anything that fits no other method are all POST. When it creates something, answer 201 Created with a Location header pointing to the new resource.
PUT replaces the whole state of the resource at the URL with the body, or creates it there if it does not exist. The notes describe PUT twice: once as “replace or update” and once, in the protocols section, as “create or full update”. The second is the complete definition. The key difference from POST is who chooses the URL: with POST /users the server picks the new ID; with PUT /users/42 or PUT /files/report.pdf the client names it. Answer 201 when PUT created the resource and 200 or 204 when it replaced one.
PATCH (added by RFC 5789 in 2010) applies a partial modification. The body is a patch document whose media type says how to apply it, so the same URL can accept different patch formats (next section).
DELETE removes the resource, or more exactly the link between the URL and what it pointed to. It is idempotent: deleting twice leaves the same state. Like GET, a body on DELETE has no defined meaning. If the deletion only happens later (a background purge), answer 202 Accepted.
OPTIONS asks what the server supports for a URL. The answer lists methods in an Allow header. Browsers send it automatically as the CORS preflight before cross-origin requests that are not “simple” (lesson 2.3). OPTIONS * asks about the server as a whole.
CONNECT asks a proxy to open a TCP tunnel: CONNECT api.example.com:443. After a 200 the proxy just copies bytes both ways, and the client runs TLS through the tunnel, so the proxy never sees the content. This is how HTTPS crosses a corporate forward proxy (lesson 1.12). HTTP/2 and HTTP/3 also have an extended CONNECT, used to run WebSockets over them (lesson 2.6).
TRACE makes the final server echo the request it received, to debug what proxies changed. Because the echo could include cookies and authorisation headers (a “cross-site tracing” attack once used it to steal them), servers and proxies disable it. You will only meet it in security scans that flag it as enabled.
QUERY is new: the IETF HTTP working group is standardising a method that is safe and idempotent like GET but carries the query in a body, for searches too large or too structured for a URL (a filter of 50 conditions, a GraphQL query, a vector search). Today such searches use POST and lose GET’s caching and retry guarantees. Check whether it has been published as an RFC and whether your framework, proxies and CDN support it before relying on it. Other extensions exist too: WebDAV added PROPFIND, MKCOL, LOCK and others for file editing, which you may still meet in file servers.
#Safe, idempotent, cacheable: three different properties
The notes define two properties; HTTP has three, and each is used by different machines.
- Safe: the client neither requests nor expects any change of state on the server. Safe is about the meaning of the request, not about the server doing nothing: logging, counting a page view or warming a cache are fine. Because safe requests are harmless by definition, browsers prefetch them, crawlers and link previews follow them, and nobody asks the user before sending them.
- Idempotent: the intended effect on the server of several identical requests is the same as of one. The response may differ (DELETE returns 204, then 404). Every safe method is idempotent; DELETE and PUT are idempotent without being safe. Because a repeat is harmless, clients, proxies and libraries may resend an idempotent request automatically when a connection fails before the answer arrives.
- Cacheable: a response to this method may be stored and reused for later requests. GET and HEAD responses are cacheable. A POST response is cacheable only with explicit freshness information and a
Content-Locationmatching the URL, which almost no cache implements. A successful PUT, PATCH, POST or DELETE invalidates what a cache stored for that URL.
| Method | Safe | Idempotent | Cacheable | Why |
|---|---|---|---|---|
| GET | ✅ | ✅ | ✅ | Only reads; the response is a copy of the resource |
| HEAD | ✅ | ✅ | ✅ | GET without the body |
| OPTIONS | ✅ | ✅ | ❌ | Only asks about capabilities; the spec says its responses are not cacheable (CORS preflights have their own Access-Control-Max-Age cache) |
| TRACE | ✅ | ✅ | ❌ | Only echoes |
| PUT | ❌ | ✅ | ❌ | Sets the final state; repeating sets the same state |
| DELETE | ❌ | ✅ | ❌ | Gone once is gone; gone twice is still gone |
| POST | ❌ | ❌ | rarely | Its meaning is up to the resource; a repeat may create a second order |
| PATCH | ❌ | not guaranteed | ❌ | Depends on the patch: “set city to Sylhet” is repeatable, “append item 7” is not |
| CONNECT | ❌ | ❌ | ❌ | Opens a new tunnel each time |
| QUERY (draft) | ✅ | ✅ | ✅ | GET semantics with a body; the cache key must include the body |
Two consequences follow directly. First, the server is responsible for keeping safe methods safe: a GET /posts/7/delete link will be followed by a prefetcher or crawler sooner or later. Second, an API that sends everything as POST (an RPC-style “tunnel”) throws away caching and automatic retries for every read.
Send it again: what repeating each method does to server state
Idempotent means the effect on the server is the same after one or many identical requests. Watch the state badge on the database, not the response.
Scenario
A JSON Merge Patch that sets a value. PATCH is not idempotent by definition, but this one behaves idempotently.
The body is a merge patch (Content-Type: application/merge-patch+json): “make city equal Sylhet”. It names the final value, not a change relative to the current one.
Self-checkQuestion
A team exposes GET /cart/add?item=7 so that a product link can add to the cart in one click. What will eventually go wrong?
#PUT vs PATCH
PUT says “make it exactly this”. Fields you leave out are not “unchanged”, they are gone (or reset to defaults). It is idempotent precisely because the result does not depend on the old state. Use it when the client holds the whole resource: a settings document, a file, a configuration object.
PATCH says “apply these changes”. How to interpret the changes depends on the body’s Content-Type, and two standard formats exist for JSON:
JSON Merge Patch (RFC 7396, application/merge-patch+json): the body looks like the resource with only the fields to change. null means “remove this field”.
PATCH /users/42 HTTP/1.1
Content-Type: application/merge-patch+json
{ "city": "Khulna", "phone": null }
It is simple and, in normal use, idempotent. Its limits: you cannot set a field to the value null, and arrays are replaced whole (you cannot “add one tag”).
JSON Patch (RFC 6902, application/json-patch+json): the body is a list of operations (add, remove, replace, move, copy, test) with JSON Pointer paths. The whole list is applied atomically: if one operation fails, nothing changes.
PATCH /carts/9 HTTP/1.1
Content-Type: application/json-patch+json
[
{ "op": "test", "path": "/version", "value": 7 },
{ "op": "add", "path": "/items/-", "value": { "sku": "A-17", "qty": 1 } },
{ "op": "replace", "path": "/coupon", "value": "EID25" }
]
It can express anything (array inserts, moves, a test that acts as a guard), but an add to /items/- (“after the last element”) is not idempotent: two copies add two items. The test operation is a built-in safety check: if version is no longer 7, the whole patch fails.
A server lists the patch formats it accepts in an Accept-Patch header and answers 415 Unsupported Media Type for any other. A patch that cannot be applied to the current state is answered with 409 Conflict (or 422).
| PUT | JSON Merge Patch | JSON Patch | |
|---|---|---|---|
| Body | The full resource | Only changed fields | A list of operations |
| Idempotent | Yes | In practice yes | No (in general) |
| Remove a field | Leave it out | "field": null | {"op": "remove"} |
| Edit inside an array | Send the whole array | Send the whole array | Yes (add, remove by index) |
| Best for | Whole documents, files | Simple form edits | Precise or conditional edits |
#Lost updates and conditional requests
Two people editing the same record can lose each other’s work even with perfect methods: both read version 1, both write, and the second write silently overwrites the first (a lost update). HTTP’s answer is optimistic concurrency with conditional requests. The server sends a version tag, the ETag, with every read; the client sends it back on the write:
If-Match: "v1": apply this only if the resource is still version v1. Otherwise 412 Precondition Failed, and nothing is written. The client re-reads and tries again.If-None-Match: *on a PUT: create only, never overwrite (412 if something already exists there). Amazon S3 has supported both conditions on writes since 2024.If-Unmodified-Since: the same idea with a timestamp, less precise.- 428 Precondition Required: the server refuses writes that come without a condition, forcing clients to protect themselves.
The read-side twins (If-None-Match with an ETag, If-Modified-Since, answered with 304) save bandwidth instead of preventing conflicts; they belong to lesson 2.4. Designing ETags and versioned APIs is part of Chapter 26.
PUT vs PATCH, lost updates and If-Match
Two editors, one user record: {name: Rahim, city: Dhaka, phone: 017…}. The ETag is the record’s version tag.
Scenario
PUT sends the complete new state. Whatever is missing is gone.
Alice only wants to change the city, so her app sends just name and city. But PUT means “make the resource exactly this”.
#Why retrying a POST is dangerous
When a request times out, the client cannot know which of three things happened: the request never arrived, the server crashed halfway, or the server finished and the response was lost. For a GET this does not matter. For a payment it is everything.
Retries happen in more places than the code you wrote:
- Browsers may resend an idempotent request when a reused connection closes before any response arrived (RFC 9112 allows this), but never a POST silently: reloading a page that was the result of a POST shows “Confirm form resubmission”.
- Proxies and load balancers retry on another upstream when one fails. Nginx has refused to pass a non-idempotent request (such as POST or PATCH) to the next server, once it was sent to one, since version 1.9.13 unless told otherwise; Envoy and service meshes have similar per-route retry policies.
- Client libraries retry by method: Python’s urllib3, for example, retries GET, HEAD, PUT, DELETE, OPTIONS and TRACE by default, never POST.
- HTTP/2 and HTTP/3 tell the client when a stream was never processed (a
GOAWAYframe names the last stream the server handled;REFUSED_STREAMrejects one). Those requests are safe to retry even if they are POSTs, because the server guarantees it did not start them. - TLS 1.3 0-RTT “early data” can be replayed by an attacker, so servers accept it only for safe requests and answer 425 Too Early otherwise (lesson 2.8).
So what about the requests that must not run twice, such as POST /payments? Make them idempotent yourself with an idempotency key: before the first attempt the client generates a unique value (usually a random UUID) and sends it in an Idempotency-Key header on that attempt and every retry. The server stores the key with the result; when the same key comes again it returns the stored response instead of doing the work again. The notes’ own example: a customer pays ৳5000 for order 500, the network times out, and the retry carries Idempotency-Key: pay-abc-123, so the money is taken once. The header was popularised by Stripe and is being standardised as an IETF draft.
Two edge cases need an answer in every implementation: a second request with the same key while the first is still running (answer 409 Conflict, “in progress”), and the same key with a different body (answer 422, because the client is misusing the key). Where to store keys, how long to keep them, and how to make the debit and the stored response commit together are designed in Chapter 26; consumers of queues and webhooks face the same problem in Chapter 30.
The response never came: who may retry, and what happens
A timeout does not tell the client whether the server did the work. The method decides whether trying again is harmless.
Scenario
One upstream instance dies mid-request. The proxy can safely try another one.
The app asks for a payment’s status.
#Status codes: the server’s answer in three digits
Every response starts with a three-digit status code. The first digit is the class, and that is what a client must understand even when it has never seen the specific code: RFC 9110 says an unknown code must be treated like the x00 code of its class (an unknown 4xx is handled as 400, an unknown 5xx as 500). That is why inventing 299 or 600 is pointless.
The text after the number (“OK”, “Not Found”) is the reason phrase. It is only for humans, clients must ignore it, and HTTP/2 and HTTP/3 do not send it at all: they carry only the number, in the :status pseudo-header. Never write code that compares the reason phrase.
| Class | Meaning | Who should act |
|---|---|---|
| 1xx Informational | An interim answer; the final response is still to come | Nobody; the client keeps going |
| 2xx Success | The request was received, understood and accepted | Nobody |
| 3xx Redirection | Further action is needed: go to another URL, or use your cached copy | The client, usually automatically |
| 4xx Client error | The request is wrong or not allowed | The client must change something before retrying (except 408, 425, 429) |
| 5xx Server error | The server, or something behind it, failed on a valid request | The server team; the client may retry later |
#1xx: informational
1xx responses are interim: a server may send one or more before the real response.
- 100 Continue. A client about to upload a 2 GB file sends the headers with
Expect: 100-continueand waits. The server either answers 100 (“go ahead, send the body”) or rejects at once with a final code such as 401 or 413 Content Too Large, saving the upload. - 101 Switching Protocols. The answer to an HTTP/1.1
Upgraderequest, mainly for WebSocket (lesson 2.6). HTTP/2 and HTTP/3 have no 101; they use extended CONNECT instead. - 103 Early Hints (RFC 8297). While the server is still building a slow page, it sends
Linkheaders so the browser can start preloading the CSS and fonts or connecting to other origins. It replaced HTTP/2 server push (lesson 2.1). - 102 Processing came from WebDAV and is deprecated.
#2xx: success
- 200 OK: the generic success. The body depends on the method: the resource for GET, a result for POST.
- 201 Created: something new exists. Include
Location: /orders/501(and often the new resource or its ETag in the body). - 202 Accepted: the request was accepted for processing but not completed, and it may still fail. The standard asynchronous pattern returns a status URL in
Location(and optionallyRetry-After); the client polls it; when the job finishes the status resource answers 303 See Other to the result. Use it whenever the work takes longer than a few seconds: reports, video transcoding, bulk imports, anything put on a queue (Chapter 30). - 204 No Content: success with an empty body, common for PUT, PATCH and DELETE. A browser that submitted a form and gets 204 stays on the same page.
- 206 Partial Content: the answer to a
Rangerequest. Video players seek with it, download managers resume and parallelise with it. An impossible range gets 416 Range Not Satisfiable.
202 Accepted: long work without a long request
Building a monthly report takes about a minute. Holding one HTTP request open that long breaks on every proxy timeout.
Scenario
The standard asynchronous request pattern.
The client asks for a report.
#3xx: redirection
A redirect tells the client to repeat the request at the URL in the Location header. The codes differ in two ways: is the move permanent (may the client and search engines remember it?) and must the client keep the method and body?
| Code | Name | Permanent? | Method on the next request | Cached by default | Use it for |
|---|---|---|---|---|---|
| 301 | Moved Permanently | Yes | Browsers turn POST into GET | Yes | Old page URLs, http:// → https:// for GET traffic |
| 302 | Found | No | Browsers turn POST into GET | No | Legacy temporary moves (login redirects) |
| 303 | See Other | No | Always GET | No | After a POST: “see the result over there” (Post/Redirect/Get), job results |
| 307 | Temporary Redirect | No | Kept (same method and body) | No | Temporary moves of APIs, maintenance |
| 308 | Permanent Redirect | Yes | Kept | Yes | Permanent moves of APIs that receive POST or PUT |
| 304 | Not Modified | (not a move) | n/a | n/a | Answer to a conditional GET: “your cached copy is still valid” |
Why do 301 and 302 change POST into GET? Early browsers did it, the behaviour became too common to fix, and the specification eventually allowed it. So 307 and 308 were added for the cases where the method must survive. Some practical rules:
- Permanent redirects are sticky. A browser may cache a 301 or 308 indefinitely; a mistake keeps sending users to the wrong place long after you fix the server. Test with 302 or 307 first.
- Post/Redirect/Get: after a successful form POST, answer 303 to a page that shows the result. Reloading that page repeats only a harmless GET.
- Search engines treat 301 and 308 as “index the new URL and move its ranking signals there”; temporary redirects keep the old URL indexed. That is the “SEO juice” in the notes; it applies to permanent redirects, not to 302.
- Loops: browsers give up after about 20 redirects (“too many redirects”). The classic cause is an app behind a TLS-terminating load balancer that sees plain HTTP and redirects to HTTPS forever; the fix is to trust
X-Forwarded-Protoor redirect only at the edge. - Open redirects: a
?next=parameter copied intoLocationwithout checking lets attackers bounce users from your trusted domain to a phishing site. Allow only relative paths or a list of known hosts. - API clients often do not follow redirects (curl needs
-L), and those that do may switch POST to GET too. Do not rely on redirects in an API; if you must move one, use 308. - 304 is in the 3xx family but is not a move: it answers a conditional GET, meaning the client’s stored copy, although no longer fresh, has been validated and is still correct, so no body is sent (the notes call it “the cached file is fresh”, which is slightly off). Lesson 2.4 covers it.
Redirects: 301 vs 308 vs 307 vs 303
Every redirect carries a Location header. What differs is whether it is permanent and whether the browser keeps the method and body.
Scenario
An API moved for good and answers 301. A form POST arrives.
An old page still posts to the old host.
#4xx: client errors
A 4xx says the problem is in the request, so repeating it unchanged will fail again (with three deliberate exceptions: 408, 425 and 429).
| Code | Name | Meaning and typical use |
|---|---|---|
| 400 | Bad Request | Malformed syntax: invalid JSON, a missing required header, a broken query string |
| 401 | Unauthorized | Unauthenticated: no credentials, or bad or expired ones. Must include WWW-Authenticate saying how to authenticate |
| 403 | Forbidden | Authenticated (or not needed) but not allowed. Logging in again will not help |
| 404 | Not Found | No resource here, or one you are not allowed to know about |
| 405 | Method Not Allowed | The resource exists but not for this method. Must include Allow: GET, HEAD |
| 406 | Not Acceptable | The server cannot produce any format listed in the client’s Accept |
| 408 | Request Timeout | The client was too slow sending the request; the server closes the connection. Safe to retry |
| 409 | Conflict | The request conflicts with the current state: duplicate username, version conflict, idempotency key still in progress |
| 410 | Gone | It existed and has been removed on purpose, permanently. Search engines drop it faster than a 404 |
| 412 | Precondition Failed | An If-Match / If-None-Match / If-Unmodified-Since condition was false |
| 413 | Content Too Large | The body exceeds the limit (Nginx’s default client_max_body_size is only 1 MB) |
| 414 | URI Too Long | The URL is too long, often a GET that should have been a POST or QUERY |
| 415 | Unsupported Media Type | The body’s Content-Type is not accepted (XML sent to a JSON-only API, an unknown patch format) |
| 422 | Unprocessable Content | Well-formed but semantically invalid: an email address without a domain, an end date before the start |
| 425 | Too Early | The server will not process replayable TLS 1.3 early data; retry after the handshake |
| 428 | Precondition Required | Writes must be conditional; send If-Match |
| 429 | Too Many Requests | Rate limited. Should include Retry-After |
| 431 | Request Header Fields Too Large | Usually a cookie that grew out of control |
| 451 | Unavailable For Legal Reasons | Blocked by a legal demand (the number is a nod to Fahrenheit 451) |
Four pairs cause most arguments:
- 401 vs 403. The names are historical. 401 is “who are you?” and invites the client to authenticate; 403 is “I know who you are, and no”. A client that answers 403 by logging the user in again creates an endless loop. Sessions and tokens are in lesson 1.3; authorisation models in Chapters 34–35.
- 403 vs 404. If revealing that something exists is itself a leak (another tenant’s invoice, a private repository), answer 404. RFC 9110 explicitly allows it, and GitHub does exactly this.
- 404 vs 410. 404 says nothing about the future; 410 promises it will not come back. Use 410 for deliberately retired content and API versions. Both are cacheable by default, so a CDN may keep answering 404 for a while after the resource appears: set an explicit
Cache-Controlon error responses. - 400 vs 422. Many APIs use 400 for everything; others use 400 for unparseable input and 422 for well-formed input that breaks a business rule. Either is fine. Pick one convention, document it, and put the details in the body.
401 → log in → 200, and why 403 does not go away
401 asks “who are you?”; logging in can fix it. 403 says “I know who you are, and no”; logging in again cannot.
Scenario
The classic flow.
The app calls the API without credentials.
429 Too Many Requests deserves its own word because it is not an error in the client’s logic; it is flow control. The server (usually an API gateway) is saying “slow down”, and Retry-After says for how long, in seconds or as an HTTP date. Well-behaved clients wait at least that long, add random jitter so that thousands of clients do not return in the same instant, and cap their attempts. Many APIs also report the quota itself: the IETF is standardising RateLimit and RateLimit-Policy headers, and older APIs send X-RateLimit-Limit and X-RateLimit-Remaining. Rate-limiting algorithms are in Chapter 29 and retry strategy in Chapter 28.
429 and 503 with Retry-After: slowing clients down politely
Limit: 100 requests per minute per API key. The gateway counts; the API behind it never sees the excess.
Scenario
A client that reads the headers.
The client has used its whole minute’s budget.
#5xx: server errors
A 5xx says the request may have been fine, but the server, or something behind it, could not complete it. The single most useful question when you see one is who generated it: your application, or a proxy in front of it?
| Code | Name | Usually generated by | Typical cause | Look at |
|---|---|---|---|---|
| 500 | Internal Server Error | The application | An unhandled exception or unexpected condition | App logs and traces for that request ID |
| 501 | Not Implemented | The server | It does not support this method at all | The client, or the server’s method list |
| 502 | Bad Gateway | A proxy / load balancer | The upstream refused or reset the connection, crashed mid-response, or sent a malformed response | Upstream process: crashes, restarts, out-of-memory kills, keep-alive timeouts |
| 503 | Service Unavailable | A proxy (no healthy targets) or the app (shedding load) | Overload, maintenance, a deploy, no capacity. Temporary by definition; send Retry-After | Health checks, capacity, deploy status |
| 504 | Gateway Timeout | A proxy / load balancer | The upstream did not answer within the proxy’s timeout (60 s by default in Nginx’s proxy_read_timeout and in AWS ALB’s idle timeout) | Whatever the app was waiting for: a slow query, another service, a lock |
| 505 | HTTP Version Not Supported | The server | A protocol version it refuses | The client |
Real platforms add their own: Nginx logs 499 when the client closed the connection before the answer (never sent on the wire, but a rising 499 rate means users are giving up); Cloudflare answers 520–527 for problems between it and your origin (521 origin refused the connection, 522 connection timed out, 524 the origin took longer than 100 seconds, 525 TLS handshake failed). On AWS, an ALB returns 503 when a target group has no registered targets; when all registered targets are unhealthy it “fails open” and sends traffic to them anyway, so the symptoms become 502 or 504.
The notes’ one-line definitions are right in spirit; two need sharpening. A 502 is not only a “wrong” response: a refused or reset connection counts too. And 503 does not mean the server is switched off: it is a temporary refusal, often by the load balancer, and often deliberate (an app protecting itself by rejecting work it cannot finish in time).
Who produced this status code? 500 vs 502 vs 503 vs 504
A reverse proxy or load balancer sits in front of the app. The 5xx code tells you which hop failed, if you know who generated it.
Scenario
The client sends GET /orders/77 to the load balancer, which picks a healthy app server.
#Which responses are safe to retry?
| Response | Retry? | How |
|---|---|---|
| Connection refused, before anything was sent | Yes | Nothing reached the server; try again or try another instance |
| Timeout / connection reset after sending | Only if idempotent or protected by an idempotency key | The server may have done the work |
| 408, 425 | Yes | Resend (for 425, after the handshake completes) |
| 429, 503 | Yes, later | Wait at least Retry-After; otherwise exponential backoff with jitter |
| 502 | If idempotent | Usually a restarting or crashed instance; another one may work |
| 504 | If idempotent, carefully | The upstream may still be working, and retries add load to whatever is already slow |
| 500 | At most once, if idempotent | Usually a bug: the same input hits the same bug |
| 401 | Once, after refreshing the token | If it fails again, the user must log in |
| 409, 412 | After re-reading the current state | Re-apply the change to the new version, or ask the user |
| Other 4xx | No | Fix the request |
Retries also multiply. If a client, a gateway and a service each try three times, one failing call at the bottom becomes 3 × 3 × 3 = 27 attempts, at the very moment the bottom service is struggling. Retry at one layer only (usually the one closest to the user), use a retry budget, and back off with jitter. Chapter 28 covers retries, backoff and circuit breakers properly.
#Error responses that help: problem details
A status code says which kind of failure; the body should say exactly what failed, in a form both programs and people can read. Instead of every API inventing its own error shape, use Problem Details for HTTP APIs (RFC 9457, 2023, which replaced RFC 7807), with Content-Type: application/problem+json:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://example.com/problems/validation",
"title": "Your request is not valid",
"status": 422,
"detail": "2 fields failed validation",
"instance": "/signup/requests/7f3a",
"errors": [
{ "field": "email", "reason": "must contain a domain" },
{ "field": "birthDate", "reason": "must be in the past" }
],
"traceId": "4bf92f3577b34da6"
}
typeis a URI that identifies the kind of problem (ideally a page that documents it); clients branch on it, not on the human text.titleis a short summary that does not change between occurrences;detailexplains this occurrence.statusrepeats the HTTP code (handy when the body is logged on its own);instanceidentifies this occurrence.- Extra members (
errors,traceId,balance) are allowed and expected.
Never put stack traces, SQL or internal hostnames in an error body: they help attackers more than users. Put a trace ID there instead and keep the details in your logs. Error format design for a whole API (codes, versioning, documentation) is part of Chapter 26.
#Anti-patterns
- 200 with an error inside (
200 OK {"success": false, "error": "db down"}). Every machine on the path reads only the status: the CDN caches the error as a success, the error-rate dashboard stays green, retry logic never fires, and every client must parse every body to find out whether it worked. (GraphQL traditionally returns 200 with anerrorsarray; the newer GraphQL-over-HTTP specification uses proper 4xx and 5xx codes for request-level failures.) - 500 for the client’s mistakes. A validation failure answered with 500 pages the on-call engineer and invites retries that can never succeed. Validation is 400 or 422.
- 404 for an empty list.
GET /orders?status=openwith no matches is200 []; 404 means the collection itself does not exist. - 403 when the user is not logged in, or 401 when they are logged in but lack permission: the client cannot tell whether to show a login screen or an “access denied” message.
- 301 for an API that receives POST: the body is dropped and the method changes. Use 308.
- GET with side effects and POST for every read: the first breaks safety, the second throws away caching and automatic retries.
- Custom status codes (
299,600): clients treat them as200and500anyway. Put the detail in the body. - Errors with no cache headers. 404, 405, 410, 414 and 501 are cacheable by default, so a CDN may keep serving a stale 404. Set
Cache-Controldeliberately on error responses (no-store, or a shortmax-age).
Error responses: why “200 with an error inside” hurts
A CDN caches successful responses for 5 minutes; a monitor counts 5xx responses. Both only read the status code.
Scenario
The database is down and the API hides it inside a 200.
A user asks for prices.
#Methods and status codes in system design
- Load balancer health checks are HTTP requests judged by status code: the ALB default healthy range is 200, and many teams use
GET /healthzanswering 200 or 503. A readiness endpoint that returns 503 while the instance is draining lets the balancer stop sending new traffic before shutdown (Chapter 25). - Monitoring and SLOs are built on status codes. The usual availability SLI counts 5xx (and timeouts) as failures and leaves 4xx out, because refusing bad input is correct behaviour. Track 4xx, 429 and 499 separately: a jump in 4xx often means a broken client release.
- Circuit breakers trip on 5xx and timeouts, not on 4xx; a 404 storm is not a reason to stop calling a healthy service (Chapter 28).
- Caches and CDNs decide what to store from the method and the status code (lesson 2.4; CDNs in Chapters 23–24).
- Async work follows the 202 + status URL + 303 pattern, or replaces polling with server-sent events or webhooks (lesson 2.5, Chapter 26).
- gRPC runs over HTTP/2 but returns HTTP 200 for almost every call and carries its own status (such as
NOT_FOUNDorUNAVAILABLE) in trailers; gateways translate between the two (Chapter 26). - Interview angle: “Is PATCH idempotent?” (not necessarily; explain merge vs JSON Patch). “Is DELETE idempotent if the second call returns 404?” (yes). “How do you make a payment API safe to retry?” (idempotency key). “502 vs 504?” (dead upstream vs slow upstream, both generated by the proxy). “Which codes do you retry?” (the table above).
#Try it yourself
curl shows the method, status line and headers with -i (response headers) or -v (everything). httpbin.org is a public test service that answers whatever you ask for.
# HEAD: headers only. Compare Content-Length with what a GET would download.
curl -I https://httpbin.org/image/png
# OPTIONS: which methods does this URL allow? (look at the Allow header)
curl -i -X OPTIONS https://httpbin.org/get
# A wrong method gives 405 with an Allow header
curl -i -X DELETE https://httpbin.org/get
# Ask for any status code to see how your client reacts
curl -i https://httpbin.org/status/429
curl -s -o /dev/null -w '%{http_code}\n' https://httpbin.org/status/503
# Redirects: curl -L follows them. Watch the method on the second request with -v.
# 302 (and 301, 303): curl switches POST to GET; 307 (and 308) keep POST and the body.
curl -v -L -X POST -d 'item=7' 'https://httpbin.org/redirect-to?url=/anything&status_code=302'
curl -v -L -X POST -d 'item=7' 'https://httpbin.org/redirect-to?url=/anything&status_code=307'
# Range request: ask for the first 100 bytes, get 206 Partial Content
curl -i -H 'Range: bytes=0-99' https://httpbin.org/range/1000
# Send an idempotency key (httpbin just echoes it back)
curl -i -X POST https://httpbin.org/anything \
-H 'Idempotency-Key: 6f1c2a9e-3c1d-4f7a-9a51-2b7e0d4c8f10' \
-H 'Content-Type: application/json' -d '{"order_id": 500, "amount": 5000}'
In the browser, open the developer tools (F12), go to the Network tab, tick Preserve log, and submit a form on any site: you will see the POST, the redirect after it (often 302 or 303) and the GET that follows, with the method of each. Reload the result page and notice whether the browser asks to resubmit.
#Summary
Key takeaways
- The method and the status code are a contract with every machine on the path (browsers, proxies, CDNs, libraries, monitors), which act on them without understanding your app. Semantics are defined once in RFC 9110 and are the same in HTTP/1.1, /2 and /3.
- GET reads (a body has no defined meaning), HEAD is GET without the body, POST processes a body by the resource’s own rules (not only “create”), PUT creates or replaces at a client-chosen URL, PATCH applies a patch document, DELETE removes, OPTIONS asks what is allowed (CORS preflight), CONNECT opens a tunnel through a proxy, TRACE echoes and is disabled. QUERY (being standardised) is a safe, idempotent GET with a body.
- Safe = no requested state change (GET, HEAD, OPTIONS, TRACE): prefetchers and crawlers send them freely. Idempotent = same effect once or many times (safe methods plus PUT and DELETE): may be retried automatically. Cacheable = GET and HEAD in practice. Idempotent is about state, not responses (DELETE → 204, then 404).
- PUT replaces everything; JSON Merge Patch sends changed fields (
nullremoves; arrays whole); JSON Patch sends atomic operations (not idempotent in general). Prevent lost updates withIf-Match+ ETag → 412. - A timeout hides whether the work happened. Proxies and libraries never auto-retry POST; make critical POSTs idempotent with an
Idempotency-Key(same key → stored response; in progress → 409; different body → 422). Full design in Chapter 26. - 1xx interim (100, 101, 103), 2xx success (200, 201 +
Location, 202 + status URL, 204, 206), 3xx redirects (301/302 may turn POST into GET, 303 always GET, 307/308 keep the method; 304 = validated cached copy), 4xx fix the request (400, 401 unauthenticated vs 403 forbidden, 404 vs 410, 405 +Allow, 409, 412, 415, 422, 429 +Retry-After), 5xx server side (500 app bug; 502 dead upstream, 503 temporarily unavailable, 504 slow upstream, usually generated by the proxy). - Retry 408, 425, 429 and 503 (after waiting), 502 and 504 only when idempotent, 500 at most once, other 4xx never; retry at one layer, with backoff and jitter.
- Return errors with the right code and an
application/problem+jsonbody (RFC 9457); never 200 with an error inside, never 500 for a validation failure.
Answer on your own first, then read the explanation, even when you got it right.
Self-checkQuestion
Which statement defines an idempotent method correctly?
Self-checkQuestion
Which two methods are safe?
Choose 2
Self-checkQuestion
A client sends DELETE /users/42 and gets 204. A retry of the same request gets 404. Is DELETE still idempotent?
Self-checkQuestion
Both POST /users and PUT /users/42 can create a user. What is the essential difference?
Self-checkQuestion
A client sends PATCH /users/42 with Content-Type application/merge-patch+json and the body {city: Khulna, phone: null}. What happens?
Self-checkQuestion
A shopping app sends PATCH /carts/9 with a JSON Patch that adds an item at /items/-. A proxy retried it after a reset and the cart now has the item twice. Which two changes prevent this?
Choose 2
Self-checkQuestion
A search API needs 40 filter conditions, too long for a URL. A developer wants to send them in the body of a GET. What is the best advice today?
Self-checkQuestion
Two support agents open the same customer record. Agent A saves a new address; ten seconds later agent B saves a new phone number from a form that still shows the old address. The new address is lost. What is the standard HTTP fix?
Self-checkQuestion
An upload service lets clients choose the file name with PUT /files/name. Two users upload different files called report.pdf. How can the API stop the second from silently replacing the first?
Self-checkQuestion
A mobile app sends POST /payments for ৳5000. The request times out. The app does not know whether the customer was charged. What should a payment API offer so the app can retry safely?
Self-checkQuestion
A client reuses idempotency key pay-abc-123 for a second payment of ৳7000 after the first ৳5000 payment succeeded with the same key. What should the server do?
Self-checkQuestion
Nginx forwards POST /payments to upstream A, which resets the connection before answering. Upstream B is healthy. With default settings, what does Nginx do?
Self-checkQuestion
After submitting an order form, users who press Reload on the confirmation page sometimes create a second order. What is the standard fix?
Self-checkQuestion
An API moves from api.example.com to api2.example.com for good. Clients send POST and PUT requests with JSON bodies. Which redirect should the old host return?
Self-checkQuestion
Which two redirect codes require the client to keep the original method and body?
Choose 2
Self-checkQuestion
A browser sends GET /logo.png with If-None-Match: abc and receives 304 Not Modified. What does that mean?
Self-checkQuestion
After putting the site behind a load balancer that terminates TLS, every page fails with too many redirects. The app redirects HTTP to HTTPS. What is wrong?
Self-checkQuestion
A logged-in user with the viewer role calls DELETE /invoices/7. What should the API return, and what should the client do?
Self-checkQuestion
Which header must a 401 Unauthorized response include?
Self-checkQuestion
In a multi-tenant SaaS, a user from company A requests /invoices/9001, which belongs to company B. Which response leaks the least?
Self-checkQuestion
Generating a yearly report takes 2–3 minutes. What is the best HTTP design?
Self-checkQuestion
A client polls GET /jobs/42 and the job has failed. What status should the poll return?
Self-checkQuestion
An API returns 429 Too Many Requests with Retry-After: 30. Thousands of clients receive it at the same moment. What should each client do?
Self-checkQuestion
Which two responses to a GET are reasonable to retry automatically (with backoff)?
Choose 2
Self-checkQuestion
Load balancer logs show 504 after 60 seconds for POST /reports, while the app logs show the same request finishing successfully after 85 seconds. Users who retried got two reports. What is the root cause, and the fix?
Self-checkQuestion
Which of these is NOT a typical cause of a 503 Service Unavailable?
Self-checkQuestion
An API returns 200 OK with the body {error: database unavailable} during an outage. Which two problems does this cause?
Choose 2
Self-checkQuestion
Which body format does RFC 9457 define for HTTP API errors?
Self-checkQuestion
An old client library receives the status code 450, which it does not know. According to HTTP semantics, how must it treat the response?
Self-checkQuestion
A mobile app calls an API gateway, which calls an orders service, which calls a payments service. Each layer retries a failed call up to 3 times. The payments service is down. How many requests can one tap of the user’s button send to payments?
Self-checkQuestion
Which two responses can a shared cache store and reuse without any explicit caching headers (heuristically cacheable by default)?
Choose 2