HTTP Status Codes as a City Map
Every answer over HTTP opens with a three-digit number. The first digit picks one of five districts, and each sign in a district is one code. Choose a sign to see what the server is saying, who acts next and where to look.
100 Continue
“Go ahead and send the rest.”
Before uploading a large photo, the browser sends only the request headers, with Expect: 100-continue. The shop answers 100, the browser sends the photo, and then the real answer arrives.
- Who acts next
- the client sends the body
- Ask again?
- not applicable; this is not a final answer
- Usually written by
- the server
- Header to read: Expect (in the request)
- 100-continue
The Gatehouse: not done yet
Notes handed out on the way in, before the real answer. Exactly one final answer from another district always follows.
curl -i -H 'Expect: 100-continue' -d @photo.jpg https://shop.example/photosThis lets a server refuse a huge upload before any of it is sent. A client that hears nothing should send the body anyway after a short wait, because an older box on the path may never pass the 100 along.
THE POINT TO KEEPA 1xx is never the last word: wait for the final answer that follows it.
101 Switching Protocols
“Agreed. This connection now speaks something else.”
The shop's live chat asks to upgrade its connection to WebSocket. After the 101, the same connection stops carrying HTTP requests and carries chat messages instead.
- Who acts next
- both sides switch protocol
- Ask again?
- not applicable; this is not a final answer
- Usually written by
- the server
- Header to read: Upgrade
- websocket
The Gatehouse: not done yet
Notes handed out on the way in, before the real answer. Exactly one final answer from another district always follows.
browser developer tools → Network → the chat request shows 101Every box on the path has to pass the upgrade along. A proxy that isn't configured to do so is a common reason a WebSocket works on a laptop and fails behind a load balancer.
THE POINT TO KEEPA 1xx is never the last word: wait for the final answer that follows it.
103 Early Hints
“Start loading these while I work.”
While the shop builds the cart page, it sends a 103 naming the stylesheet the page will need. The browser starts fetching it, and the 200 with the page follows.
- Who acts next
- the client may start fetching the hinted files
- Ask again?
- not applicable; this is not a final answer
- Usually written by
- the server
- Header to read: Link
- </app.css>; rel=preload; as=style
The Gatehouse: not done yet
Notes handed out on the way in, before the real answer. Exactly one final answer from another district always follows.
curl -i https://shop.example/cartOne request can produce several status lines. A log or tool that records one number per request shows only the final answer.
THE POINT TO KEEPA 1xx is never the last word: wait for the final answer that follows it.
200 OK
“Here is what you asked for.”
The browser asks for /cart and gets the cart page.
- Who acts next
- the client uses the answer
- Ask again?
- no need
- Usually written by
- the app, or a cache holding its answer
Market Square: it worked
The server understood the request, accepted it and carried it out. Which sign you reach says what comes back.
curl -si https://shop.example/cart | head -1An app that catches its own failure and still answers 200 with {"error": "…"} in the body looks healthy to every load balancer and dashboard that reads only the number. Also, only the digits are protocol: "200 Fine" is a valid 200, and HTTP/2 sends no words at all.
RFC 9110: successful 2xx ↗RFC 9112: the HTTP/1.1 status line ↗
THE POINT TO KEEPAny 2xx says the request was carried out. It does not say the content is what you hoped for.
201 Created
“Done, and something new now exists.”
A POST to /orders creates order 7. The Location header gives the new order's address.
- Who acts next
- the client uses the answer
- Ask again?
- no need
- Usually written by
- the app
- Header to read: Location
- /orders/7
Market Square: it worked
The server understood the request, accepted it and carried it out. Which sign you reach says what comes back.
curl -si -d '{"item":12,"qty":2}' https://shop.example/orders | grep -i '^location'A client that needs the new thing should read Location and not guess the address from the request.
THE POINT TO KEEPAny 2xx says the request was carried out. It does not say the content is what you hoped for.
202 Accepted
“Received. The work will happen later.”
The browser asks for a full year's sales export. The shop puts the job in a queue and answers straight away, before any of the export exists.
- Who acts next
- the client checks back for the result
- Ask again?
- no; ask for the result at the address the API gives you
- Usually written by
- the app
Market Square: it worked
The server understood the request, accepted it and carried it out. Which sign you reach says what comes back.
a status address in the response body or Location headerA 202 is a promise to try. The job can still fail afterwards, and HTTP has no way to send that news later, so the API has to offer somewhere to ask.
THE POINT TO KEEPAny 2xx says the request was carried out. It does not say the content is what you hoped for.
204 No Content
“Done. There is nothing to send back.”
DELETE /cart/items/3 removes the item from the cart. The answer ends after its headers.
- Who acts next
- the client uses the answer
- Ask again?
- no need
- Usually written by
- the app
Market Square: it worked
The server understood the request, accepted it and carried it out. Which sign you reach says what comes back.
curl -si -X DELETE https://shop.example/cart/items/3Client code that parses every successful answer as JSON fails here, because the body is empty.
THE POINT TO KEEPAny 2xx says the request was carried out. It does not say the content is what you hoped for.
206 Partial Content
“Here is the slice you asked for.”
Someone skips ahead in the shop's intro video. The browser asks for just those bytes with a Range header and gets just those bytes.
- Who acts next
- the client uses the answer
- Ask again?
- no need
- Usually written by
- the app, a file server or a CDN
- Header to read: Content-Range
- bytes 0-999/48000
Market Square: it worked
The server understood the request, accepted it and carried it out. Which sign you reach says what comes back.
curl -si -r 0-999 https://shop.example/intro.mp4 | head -5A server is free to ignore Range and answer 200 with the whole file, so a client has to check which of the two it received.
THE POINT TO KEEPAny 2xx says the request was carried out. It does not say the content is what you hoped for.
301 Moved Permanently
“Moved for good. Use the new address from now on.”
The sale page moved from /sale to /deals. The shop answers with the new address in a Location header, and the browser sends a second request there.
- Who acts next
- the client asks again at the address in Location
- Ask again?
- yes, at the new address
- Usually written by
- the app or the proxy
- Header to read: Location
- /deals
The Crossroads: look elsewhere
What you want is at another address, or already in your own cache. The client has one more step to take.
curl -sIL https://shop.example/saleBrowsers and curl turn a redirected POST into a GET here and drop its body. Browsers may also remember a 301 long after you change the server, so use 302 until you are sure of the move. They give up after 20 redirects in a row.
RFC 9110: redirection 3xx ↗Fetch standard: following a redirect ↗
THE POINT TO KEEPA 3xx is an instruction to the client, and following it is a second request the client makes.
302 Found
“It is over there for now. Keep using this address.”
During a promotion, /deals sends visitors to /summer. Next week it will point somewhere else, so the browser keeps asking /deals first.
- Who acts next
- the client asks again at the address in Location
- Ask again?
- yes, at the new address
- Usually written by
- the app or the proxy
- Header to read: Location
- /summer
The Crossroads: look elsewhere
What you want is at another address, or already in your own cache. The client has one more step to take.
curl -sI https://shop.example/deals | grep -i '^location'The method changes just as it does for 301: a redirected POST arrives as a GET. Use 307 when it must stay a POST.
RFC 9110: redirection 3xx ↗Fetch standard: following a redirect ↗
THE POINT TO KEEPA 3xx is an instruction to the client, and following it is a second request the client makes.
303 See Other
“Done. Now go and look at this page.”
The checkout form is posted, the shop takes the order, and it redirects to /receipt/7. Refreshing the receipt repeats a GET, so the order isn't submitted twice.
- Who acts next
- the client asks again at the address in Location
- Ask again?
- yes, at the new address
- Usually written by
- the app
- Header to read: Location
- /receipt/7
The Crossroads: look elsewhere
What you want is at another address, or already in your own cache. The client has one more step to take.
curl -sL -o /dev/null -w '%{method} %{url_effective}\n' -d a=1 https://shop.example/checkoutThe second request is a GET by design, whatever the first one was. This is the usual answer to a form submission.
THE POINT TO KEEPA 3xx is an instruction to the client, and following it is a second request the client makes.
304 Not Modified
“Nothing changed. Use the copy you already have.”
The browser kept the shop's logo along with its version label, "v7". On the next visit it asks for the logo only if the label has changed. It hasn't, so the shop sends headers and no image.
- Who acts next
- the client shows its stored copy
- Ask again?
- no need
- Usually written by
- the app, a proxy or a CDN
- Header to read: If-None-Match (in the request)
- "v7"
The Crossroads: look elsewhere
What you want is at another address, or already in your own cache. The client has one more step to take.
curl -sI -H 'If-None-Match: "v7"' https://shop.example/logo.pngThis is why 304 lives at the crossroads: the answer is elsewhere, and elsewhere is the browser's own cache. It saves the transfer and not the round trip.
THE POINT TO KEEPA 3xx is an instruction to the client, and following it is a second request the client makes.
307 Temporary Redirect
“Over there for now. Repeat the request exactly.”
The checkout form is posted to /checkout while payments are being served from /pay. The browser sends the same POST, with the same body, to /pay.
- Who acts next
- the client asks again at the address in Location
- Ask again?
- yes, at the new address
- Usually written by
- the app or the proxy
- Header to read: Location
- /pay
The Crossroads: look elsewhere
What you want is at another address, or already in your own cache. The client has one more step to take.
curl -sL -o /dev/null -w '%{method}\n' -d a=1 https://shop.example/checkout307 exists because clients rewrite the method on 302. The client must not change it here.
THE POINT TO KEEPA 3xx is an instruction to the client, and following it is a second request the client makes.
308 Permanent Redirect
“Moved for good. Repeat the request exactly.”
The shop's API moved from /api/orders to /v2/orders. A POST to the old address is sent again, unchanged, to the new one.
- Who acts next
- the client asks again at the address in Location
- Ask again?
- yes, at the new address
- Usually written by
- the app or the proxy
- Header to read: Location
- /v2/orders
The Crossroads: look elsewhere
What you want is at another address, or already in your own cache. The client has one more step to take.
curl -sIL -X POST https://shop.example/api/ordersIt is to 301 what 307 is to 302. Like a 301, it may be remembered by browsers and caches, so it is hard to take back.
THE POINT TO KEEPA 3xx is an instruction to the client, and following it is a second request the client makes.
400 Bad Request
“I can't read this request.”
An order arrives with its JSON cut off partway through. The shop can't parse it, so it never gets as far as thinking about an order.
- Who acts next
- whoever sent the request
- Ask again?
- not until the request changes
- Usually written by
- the app, or a proxy that can't parse the request
The Wall: fix the request
The server looked at the request and found the problem in it: who is asking, where, or what was sent. It turns the same request back the same way every time.
curl -s -H 'Content-Type: application/json' -d '{"item": 12,' https://shop.example/ordersMany APIs answer 400 for any invalid input, not only for unreadable requests. The response body normally says what was wrong.
THE POINT TO KEEPA 4xx fails the same way until the request changes. Only 429 is fixed by waiting.
401 Unauthorized
“I don't know who you are.”
A request for /account arrives with no login. The shop refuses and names the kind of login it accepts.
- Who acts next
- whoever sent the request
- Ask again?
- yes, with valid credentials
- Usually written by
- the app, or an authenticating proxy
- Header to read: WWW-Authenticate
- Bearer realm="shop"
The Wall: fix the request
The server looked at the request and found the problem in it: who is asking, where, or what was sent. It turns the same request back the same way every time.
curl -si https://shop.example/account | head -3The name says unauthorized and the meaning is unauthenticated. The test that separates it from 403: could logging in change the answer?
THE POINT TO KEEPA 4xx fails the same way until the request changes. Only 429 is fixed by waiting.
403 Forbidden
“I understood, and the answer is no.”
Maya is logged in and asks for /admin. Maya's account is not an admin, so sending the same credentials again changes nothing.
- Who acts next
- whoever sent the request
- Ask again?
- not with the same credentials
- Usually written by
- the app, or a firewall in front of it
The Wall: fix the request
The server looked at the request and found the problem in it: who is asking, where, or what was sent. It turns the same request back the same way every time.
compare the proxy's log with the app's log for that requestA 403 is not always about your account. A firewall can answer 403 for a blocked address or a request that looks like an attack, and then the app never sees it. A server may also answer 404 in place of 403 to avoid confirming that a private page exists.
THE POINT TO KEEPA 4xx fails the same way until the request changes. Only 429 is fixed by waiting.
404 Not Found
“Nothing is at this address.”
A typo sends the browser to /crat. The shop has no page there.
- Who acts next
- whoever sent the request
- Ask again?
- not until the request changes
- Usually written by
- the app, or a proxy with no rule for the path
The Wall: fix the request
The server looked at the request and found the problem in it: who is asking, where, or what was sent. It turns the same request back the same way every time.
curl -si https://shop.example/crat | head -1When the proxy writes the 404, the path may be right and the routing wrong, and the app's log is empty. A 404 doesn't say whether the page ever existed, and caches may store it, so a CDN can keep answering 404 for a file that was missing only during a deploy.
THE POINT TO KEEPA 4xx fails the same way until the request changes. Only 429 is fixed by waiting.
405 Method Not Allowed
“This address exists. That action isn't offered on it.”
A DELETE is sent to /orders. Orders can be read or added there, and the list itself can't be deleted.
- Who acts next
- whoever sent the request
- Ask again?
- not until the request changes
- Usually written by
- the app
- Header to read: Allow
- GET, POST
The Wall: fix the request
The server looked at the request and found the problem in it: who is asking, where, or what was sent. It turns the same request back the same way every time.
curl -si -X DELETE https://shop.example/orders | grep -i '^allow'A request names a place, the path, and an action, the method. 404 is about the place and 405 about the action, and a 405 has to list the actions that are allowed.
THE POINT TO KEEPA 4xx fails the same way until the request changes. Only 429 is fixed by waiting.
409 Conflict
“Valid, but it clashes with how things are now.”
An order for item 12 arrives a moment after the last one sold. Nothing is wrong with the request itself.
- Who acts next
- whoever sent the request
- Ask again?
- after loading the current state and deciding again
- Usually written by
- the app
The Wall: fix the request
The server looked at the request and found the problem in it: who is asking, where, or what was sent. It turns the same request back the same way every time.
the response bodySending the identical request again usually conflicts again. The client has to look at the current state first.
THE POINT TO KEEPA 4xx fails the same way until the request changes. Only 429 is fixed by waiting.
422 Unprocessable Content
“Readable, and the values make no sense.”
An order arrives as well-formed JSON asking for a quantity of -3.
- Who acts next
- whoever sent the request
- Ask again?
- not until the request changes
- Usually written by
- the app
The Wall: fix the request
The server looked at the request and found the problem in it: who is asking, where, or what was sent. It turns the same request back the same way every time.
curl -s -H 'Content-Type: application/json' -d '{"item":12,"qty":-3}' https://shop.example/ordersThree refusals at three depths: 400 could not read the request, 422 read it and found the values wrong, 409 found the values fine and the current data in the way.
THE POINT TO KEEPA 4xx fails the same way until the request changes. Only 429 is fixed by waiting.
429 Too Many Requests
“Too many requests from you. Slow down.”
A script checking prices speeds up past the shop's limit. The shop refuses the extra requests and says how long to wait.
- Who acts next
- whoever sent the request
- Ask again?
- yes, after waiting
- Usually written by
- a rate limiter in the app, the proxy or a CDN
- Header to read: Retry-After
- 30
The Wall: fix the request
The server looked at the request and found the problem in it: who is asking, where, or what was sent. It turns the same request back the same way every time.
curl -sI 'https://shop.example/search?q=mug' | grep -i '^retry-after'Retry-After is optional. When it is missing, wait longer after each refusal and add some randomness, so that many limited clients don't all return in the same second.
THE POINT TO KEEPA 4xx fails the same way until the request changes. Only 429 is fixed by waiting.
500 Internal Server Error
“Something broke in here.”
The shop's code hits a case it does not handle while building the cart. Its framework catches the crash and answers 500, and the proxy passes that answer back unchanged.
- Who acts next
- whoever runs the server
- Ask again?
- possibly later; nothing in the request needs fixing
- Usually written by
- the app
The Factory: the server failed
The request was fine and the city's own machinery was not. Two buildings write codes here: the front office, which is a proxy, and the workshop behind it, which is the app.
awk '$9 ~ /^5/' access.logThe page a visitor gets is vague on purpose, since a stack trace would tell an attacker about the code. The useful detail is in the app's log for that moment.
THE POINT TO KEEPA 5xx sends you to the server's logs, and a 502 or 504 points one hop behind the box that wrote it.
502 Bad Gateway
“The box behind me gave a bad answer.”
The proxy passes the request to the app and gets back half a reply, nonsense, or a closed connection. It can't forward that, so it writes an answer of its own.
- Who acts next
- whoever runs the server
- Ask again?
- often, after a short wait
- Usually written by
- the proxy, about the app
The Factory: the server failed
The request was fine and the city's own machinery was not. Two buildings write codes here: the front office, which is a proxy, and the workshop behind it, which is the app.
proxy: 502 at 10:02:11 · app: restarted at 10:02:10The app this code describes never writes it. Look for app crashes and restarts at that time, or an app that closes idle connections sooner than the proxy expects.
RFC 9110: server error 5xx ↗AWS: troubleshoot Application Load Balancers ↗
THE POINT TO KEEPA 5xx sends you to the server's logs, and a 502 or 504 points one hop behind the box that wrote it.
503 Service Unavailable
“Nobody can take this right now.”
The proxy has no copy of the app it can send to: they are starting up, failing health checks, or full. It answers without the request reaching any of the shop's code.
- Who acts next
- whoever runs the server
- Ask again?
- yes, after waiting
- Usually written by
- the proxy, or an overloaded app
- Header to read: Retry-After
- 120
The Factory: the server failed
The request was fine and the city's own machinery was not. Two buildings write codes here: the front office, which is a proxy, and the workshop behind it, which is the app.
the health-check status of each app instanceServe a maintenance page with 503 and not 200, so that clients, monitors and crawlers know the state is temporary.
RFC 9110: server error 5xx ↗RFC 9110: Retry-After ↗AWS: troubleshoot Application Load Balancers ↗
THE POINT TO KEEPA 5xx sends you to the server's logs, and a 502 or 504 points one hop behind the box that wrote it.
504 Gateway Timeout
“The box behind me took too long.”
The app has the request and is still busy when the proxy's 60-second timer runs out. The proxy stops waiting and answers 504. The app may go on to finish the work.
- Who acts next
- whoever runs the server
- Ask again?
- with care: the work may have been done
- Usually written by
- the proxy, about the app
The Factory: the server failed
The request was fine and the city's own machinery was not. Two buildings write codes here: the front office, which is a proxy, and the workshop behind it, which is the app.
proxy: 504 at 10:02:11 · app: that request still runningAfter a 504 on a POST, the order may have been saved, and sending it again creates a second one. GET, PUT and DELETE are safe to repeat. The 60 seconds is an example and also the default in nginx and on an AWS Application Load Balancer. If the client gives up first, no answer is sent at all, and nginx records its own 499 in the log.
RFC 9110: server error 5xx ↗RFC 9110: idempotent methods ↗AWS: troubleshoot Application Load Balancers ↗nginx: proxy_read_timeout ↗nginx source: the 499 log code ↗
THE POINT TO KEEPA 5xx sends you to the server's logs, and a 502 or 504 points one hop behind the box that wrote it.