{"course":{"slug":"cloud-and-shipping","title":"Shipping It","url":"https://addaly.com/learn/cloud-and-shipping","level":"intermediate","track":"build"},"lesson":{"slug":"http-and-status-codes","title":"What a status code is telling you","url":"https://addaly.com/learn/cloud-and-shipping/http-and-status-codes","position":5,"of":74,"minutes":7,"answer":"The first digit says who has to fix it; a status code that lies makes every layer above you lie too.","keyIdea":"The first digit says who has to fix it; a status code that lies makes every layer above you lie too."},"body":{"format":"markdown","text":"## A request is just text\n\nStrip away the libraries and an HTTP request is a few lines sent over a socket:\n\n```\nGET /rooms/chai HTTP/1.1\nHost: addaly.com\nAccept: text/html\nCookie: session=abc123\n```\n\nAnd the response:\n\n```\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\nCache-Control: public, max-age=60\n\n<!doctype html>...\n```\n\nA method, a path, headers, an optional body. A status line, headers, an optional body. Everything else (REST, GraphQL, your framework) is a convention layered on top of that.\n\n## The first digit answers \"whose problem is this\"\n\nThis is the part worth internalising.\n\n- **2xx**: it worked.\n- **3xx**: look somewhere else.\n- **4xx**: the request was wrong. The caller has to change something.\n- **5xx**: the request was fine and the server failed. You have to change something.\n\nA graph with two lines, 4xx and 5xx, tells you at a glance whether to open your editor or open a conversation with whoever is calling you. Almost no other single signal is that cheap.\n\n## The distinctions that bite\n\n**301 versus 302.** A 301 is permanent, and browsers cache it aggressively, sometimes until the user clears their profile. Ship a wrong 301 and you cannot take it back from the people who received it. While you are experimenting, use 302 or 307. Switch to 301 when you are certain.\n\n**401 versus 403.** 401 means \"I do not know who you are, send credentials\". 403 means \"I know exactly who you are, and no\". Send 401 to a logged-in user who lacks permission and their client will try to re-authenticate forever, in a loop, against a login they already completed.\n\n**429.** Too many requests. Pair it with a `Retry-After: 30` header so the caller knows to wait rather than retry immediately and make things worse.\n\n**502 versus 500 versus 504.** A 500 is your code throwing. A 502 or 504 comes from a proxy in front of your app: 502 means your app answered with garbage or not at all, 504 means it did not answer in time. If you see 502s, your process probably crashed or failed to start. If you see 504s, something in your handler is slower than the proxy's timeout, often 30 or 60 seconds.\n\n## Do not lie in the status line\n\nThis is common and expensive:\n\n```json\nHTTP/1.1 200 OK\n{ \"ok\": false, \"error\": \"expired token\" }\n```\n\nEvery layer between you and the user reads the status code, not your JSON. Your uptime monitor reads it. Your CDN reads it and may now cache the failure for a minute. A client library's retry logic reads it and does not retry. Your error dashboard reads it and stays green while people are locked out.\n\nThe status code is not decoration on top of the real answer. It is the summary of the outcome, written where the rest of the internet can see it. Say 401 when you mean 401.\n\n## GET must not change anything\n\nA `GET /posts/12/delete` link will eventually be followed by a crawler, a link preview bot in a chat app, a browser prefetching what it thinks you will click, or an email scanner opening every URL in a message to check for malware. All of these will delete post 12 with no human involved.\n\nThis is not a style preference. GET and HEAD are defined as safe: meaning callers are entitled to assume they cause nothing. Destructive actions go behind POST or DELETE."},"check":{"question":"An API returns HTTP 200 with `{\"ok\": false, \"error\": \"expired token\"}` when a session expires. During an incident, users were locked out for three hours while the uptime dashboard stayed green. What did that 200 actually cost?","options":[{"text":"Every layer that reads status codes (monitors, caches, retry logic) was told the request succeeded","correct":true},{"text":"Nothing structural; the monitoring should have been configured to parse the response body","correct":false,"tempting":"Treats the body as the real contract and the status line as decoration, which works only for callers you personally wrote and configured."},{"text":"The 200 is technically correct, because the HTTP transaction itself succeeded: the failure is at the application layer","correct":false,"tempting":"A genuinely common reading: the bytes were delivered fine. But HTTP status describes the outcome of the request, not the health of the pipe."},{"text":"The real fault is the expired token; the status code choice is a separate style question","correct":false,"tempting":"Focuses on the triggering event rather than on why three hours passed before anyone knew."}]},"prev":{"slug":"dns-and-domains","title":"Why your site is not loading","url":"https://addaly.com/learn/cloud-and-shipping/dns-and-domains"},"next":{"slug":"tls-and-the-padlock","title":"The padlock, and what it does not prove","url":"https://addaly.com/learn/cloud-and-shipping/tls-and-the-padlock"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'What a status code is telling you', in Shipping It. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/cloud-and-shipping/http-and-status-codes (Accessed: 31 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"What a status code is telling you","courseTitle":"Shipping It","module":{"index":1,"slug":"how-the-internet-reaches-you","title":"How the internet reaches your process"},"answer":null,"keyIdea":"The first digit says who has to fix it; a status code that lies makes every layer above you lie too.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"a-request-is-just-text","runs":[{"text":"A request is just text"}]},{"t":"p","runs":[{"text":"Strip away the libraries and an HTTP request is a few lines sent over a socket:"}]},{"t":"code","lang":"","label":"Text","text":"GET /rooms/chai HTTP/1.1\nHost: addaly.com\nAccept: text/html\nCookie: session=abc123"},{"t":"p","runs":[{"text":"And the response:"}]},{"t":"code","lang":"","label":"Text","text":"HTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\nCache-Control: public, max-age=60\n\n<!doctype html>..."},{"t":"p","runs":[{"text":"A method, a path, headers, an optional body. A status line, headers, an optional body. Everything else (REST, GraphQL, your framework) is a convention layered on top of that."}]},{"t":"h2","id":"the-first-digit-answers-whose-problem-is-this","runs":[{"text":"The first digit answers “whose problem is this”"}]},{"t":"p","runs":[{"text":"This is the part worth internalising."}]},{"t":"ul","items":[[{"text":"2xx","b":true},{"text":": it worked."}],[{"text":"3xx","b":true},{"text":": look somewhere else."}],[{"text":"4xx","b":true},{"text":": the request was wrong. The caller has to change something."}],[{"text":"5xx","b":true},{"text":": the request was fine and the server failed. You have to change something."}]]},{"t":"p","runs":[{"text":"A graph with two lines, 4xx and 5xx, tells you at a glance whether to open your editor or open a conversation with whoever is calling you. Almost no other single signal is that cheap."}]},{"t":"h2","id":"the-distinctions-that-bite","runs":[{"text":"The distinctions that bite"}]},{"t":"p","runs":[{"text":"301 versus 302.","b":true},{"text":" A 301 is permanent, and browsers cache it aggressively, sometimes until the user clears their profile. Ship a wrong 301 and you cannot take it back from the people who received it. While you are experimenting, use 302 or 307. Switch to 301 when you are certain."}]},{"t":"p","runs":[{"text":"401 versus 403.","b":true},{"text":" 401 means “I do not know who you are, send credentials”. 403 means “I know exactly who you are, and no”. Send 401 to a logged-in user who lacks permission and their client will try to re-authenticate forever, in a loop, against a login they already completed."}]},{"t":"p","runs":[{"text":"429.","b":true},{"text":" Too many requests. Pair it with a "},{"text":"Retry-After: 30","code":true},{"text":" header so the caller knows to wait rather than retry immediately and make things worse."}]},{"t":"p","runs":[{"text":"502 versus 500 versus 504.","b":true},{"text":" A 500 is your code throwing. A 502 or 504 comes from a proxy in front of your app: 502 means your app answered with garbage or not at all, 504 means it did not answer in time. If you see 502s, your process probably crashed or failed to start. If you see 504s, something in your handler is slower than the proxy’s timeout, often 30 or 60 seconds."}]},{"t":"h2","id":"do-not-lie-in-the-status-line","runs":[{"text":"Do not lie in the status line"}]},{"t":"p","runs":[{"text":"This is common and expensive:"}]},{"t":"code","lang":"json","label":"JSON","text":"HTTP/1.1 200 OK\n{ \"ok\": false, \"error\": \"expired token\" }"},{"t":"p","runs":[{"text":"Every layer between you and the user reads the status code, not your JSON. Your uptime monitor reads it. Your CDN reads it and may now cache the failure for a minute. A client library’s retry logic reads it and does not retry. Your error dashboard reads it and stays green while people are locked out."}]},{"t":"p","runs":[{"text":"The status code is not decoration on top of the real answer. It is the summary of the outcome, written where the rest of the internet can see it. Say 401 when you mean 401."}]},{"t":"h2","id":"get-must-not-change-anything","runs":[{"text":"GET must not change anything"}]},{"t":"p","runs":[{"text":"A "},{"text":"GET /posts/12/delete","code":true},{"text":" link will eventually be followed by a crawler, a link preview bot in a chat app, a browser prefetching what it thinks you will click, or an email scanner opening every URL in a message to check for malware. All of these will delete post 12 with no human involved."}]},{"t":"p","runs":[{"text":"This is not a style preference. GET and HEAD are defined as safe: meaning callers are entitled to assume they cause nothing. Destructive actions go behind POST or DELETE."}]}],"check":{"question":[{"text":"An API returns HTTP 200 with "},{"text":"{\"ok\": false, \"error\": \"expired token\"}","code":true},{"text":" when a session expires. During an incident, users were locked out for three hours while the uptime dashboard stayed green. What did that 200 actually cost?"}],"options":[{"text":[{"text":"Every layer that reads status codes (monitors, caches, retry logic) was told the request succeeded"}],"correct":true,"tempting":null},{"text":[{"text":"Nothing structural; the monitoring should have been configured to parse the response body"}],"correct":false,"tempting":[{"text":"Treats the body as the real contract and the status line as decoration, which works only for callers you personally wrote and configured."}]},{"text":[{"text":"The 200 is technically correct, because the HTTP transaction itself succeeded: the failure is at the application layer"}],"correct":false,"tempting":[{"text":"A genuinely common reading: the bytes were delivered fine. But HTTP status describes the outcome of the request, not the health of the pipe."}]},{"text":[{"text":"The real fault is the expired token; the status code choice is a separate style question"}],"correct":false,"tempting":[{"text":"Focuses on the triggering event rather than on why three hours passed before anyone knew."}]}],"order":[1,3,2,0],"why":null},"prev":{"slug":"dns-and-domains","title":"Why your site is not loading","minutes":8},"next":{"slug":"tls-and-the-padlock","title":"The padlock, and what it does not prove","minutes":9}}}