HTTP Status Codes in Practice: 200 to 503
A practical guide to HTTP status codes with real server logs, when to use 301 vs 302, and what codes mean behind a CDN or load balancer.
Our monitoring dashboard showed a spike in 499 errors. The team spent an hour searching RFC docs before realizing: 499 isn't an official HTTP status code. It's Nginx's way of saying the client disconnected before the server could respond. The client gave up waiting, Nginx logged a code that doesn't exist in any specification, and we chased a ghost through documentation that couldn't mention it.
That incident made something clear. Knowing what HTTP status codes mean on paper is one thing. Knowing what they mean when they show up in your logs at 2 AM is something else entirely. This guide covers both.
The Five Classes: A 30-Second Mental Model
Every HTTP status code is a three-digit number, and the first digit tells you the category. Memorize these five buckets and you can make a reasonable guess about any code you encounter, even one you've never seen before.
- 1xx - "Hold on." The server received the request and is still processing. You'll rarely see these in logs. The most common is
100 Continue, which tells a client it's okay to send a large request body. - 2xx - "Here you go." The request succeeded. This is what you want to see.
- 3xx - "Go look over there." The resource moved. The server is telling the client to make a new request to a different URL.
- 4xx - "You messed up." Something is wrong with the request. Bad syntax, missing authentication, or a resource that doesn't exist.
- 5xx - "I messed up." The server failed to fulfill a valid request. The client did everything right, but the server broke.
This mental model holds up surprisingly well. When you see a 503 in your logs, you immediately know the problem is on the server side. When you see a 403, you know the client sent something the server won't accept. The first digit narrows the problem space before you even look up the specific code.
The Codes You'll See Every Day
Success Codes
200 OK is the standard response for a successful request. GET returns data, POST confirms an action. It's the code you want and the code you'll see most often.
192.168.1.42 - - [29/Mar/2026:10:15:32 +0000] "GET /api/users/47 HTTP/1.1" 200 1523
192.168.1.42 - - [29/Mar/2026:10:15:33 +0000] "POST /api/orders HTTP/1.1" 201 847
192.168.1.42 - - [29/Mar/2026:10:15:34 +0000] "DELETE /api/sessions/abc HTTP/1.1" 204 0 201 Created means the server made something new. A new user account, a new database record, a new file. The response usually includes a Location header pointing to the newly created resource. If your POST endpoint creates a resource and returns 200 instead of 201, it works but it's lying about what happened.
204 No Content says "it worked, and I have nothing to send back." DELETE endpoints use this frequently. So do PUT endpoints that update a resource without returning the updated version. The response body must be empty. If you send a body with a 204, some clients will ignore it and others will throw an error.
Redirection Codes
Redirects are where things get confusing. There are four codes that seem to do the same thing, and the differences matter more than you'd expect.
301 Moved Permanently tells clients and search engines that this URL has moved forever. Browsers cache 301 redirects aggressively. Google transfers the old URL's page rank to the new one. Use this when you've permanently changed a URL structure.
302 Found is a temporary redirect. The original URL is still valid, but right now the client should go somewhere else. Login pages commonly return 302 to send you to a dashboard after authentication. Search engines keep indexing the original URL.
307 Temporary Redirect does the same thing as 302 with one critical difference: it preserves the HTTP method. A POST to a URL that returns 302 might be resent as a GET (browsers historically did this). A 307 guarantees the POST remains a POST. If your API redirects a POST request, use 307, not 302.
308 Permanent Redirect is the method-preserving version of 301. Same permanent semantics, but the client must keep the original method. This matters for APIs where a POST endpoint gets permanently relocated.
Here's the decision tree in plain text:
Is the redirect permanent?
├── Yes
│ ├── Will the client send POST/PUT/DELETE? → 308
│ └── Only GET requests? → 301
└── No (temporary)
├── Will the client send POST/PUT/DELETE? → 307
└── Only GET requests? → 302 304 Not Modified deserves its own mention. The client sends a conditional request with If-None-Match or If-Modified-Since, and the server says "nothing changed, use your cached copy." No body is sent. This is how browsers avoid re-downloading resources they already have.
Client Error Codes
400 Bad Request is the catch-all for malformed requests. Missing required fields, invalid JSON, wrong data types. When your server can't parse what the client sent, return a 400.
// Request
POST /api/users HTTP/1.1
Content-Type: application/json
{"email": "not-an-email", "age": "twenty"}
// Response
HTTP/1.1 400 Bad Request
{"error": "Validation failed", "details": ["Invalid email format", "age must be integer"]} 401 Unauthorized vs 403 Forbidden trips people up constantly. The names are misleading. 401 means "I don't know who you are." The client didn't send credentials, or the credentials are expired. Sending valid credentials should fix it. 403 means "I know who you are, and you can't do this." The client is authenticated but lacks permission. Re-sending credentials won't help.
# No token sent → 401
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/admin
401
# Valid token, but user is not an admin → 403
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer user_token" https://api.example.com/admin
403 404 Not Found needs no introduction. The resource doesn't exist at this URL. What's worth noting: some APIs return 404 for resources that exist but the user doesn't have permission to see. This prevents information leakage. If a private repository returns 403 to unauthorized users, an attacker knows the repo exists. Returning 404 hides that fact.
429 Too Many Requests means rate limiting kicked in. The response should include a Retry-After header telling the client when to try again. Well-behaved API clients read this header and back off. Poorly written ones keep hammering and make the problem worse.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1743249362
{"error": "Rate limit exceeded. Try again in 30 seconds."} Server Error Codes
500 Internal Server Error is the server equivalent of a shrug. Something went wrong, and the server doesn't have a more specific error code for it. Unhandled exceptions, null pointer errors, database connection failures that aren't caught properly. If you're seeing 500s in production, check your application logs, not your access logs. The access log just tells you it happened; the application log tells you why.
502 Bad Gateway means a proxy or load balancer tried to forward your request to an upstream server and got a garbage response (or no response at all). The reverse proxy is working fine. The application server behind it is not. This is the code you see when your Node.js app crashes but Nginx is still running.
# Nginx error log during a 502
2026/03/29 14:22:01 [error] 1234#0: *5678 connect() failed
(111: Connection refused) while connecting to upstream,
client: 10.0.0.1, upstream: "http://127.0.0.1:3000/api/data" 503 Service Unavailable means the server is temporarily unable to handle the request. This could be planned maintenance or an overloaded server. The key difference from 500: a 503 implies the condition is temporary. Include a Retry-After header so clients know when to come back.
HTTP/1.1 503 Service Unavailable
Retry-After: 120
Content-Type: application/json
{"error": "Server under maintenance. Expected back in 2 minutes."} 504 Gateway Timeout is the timeout version of 502. The proxy waited for the upstream server to respond and gave up. Your backend is running, but it's too slow. Maybe a database query is hanging, or an external API call isn't returning. The default timeout in Nginx is 60 seconds (proxy_read_timeout), which is generous for most endpoints and painfully short for report generation.
Behind the CDN: Codes That Mean Something Different
If your traffic flows through Cloudflare, AWS ALB, or Nginx, you'll encounter status codes that aren't in RFC 9110. These are vendor-specific extensions, and they're confusing if you don't know they exist.
Cloudflare's 5xx codes live in the 520-527 range. 520 means Cloudflare got an empty or unexpected response from your origin. 521 means your web server is down. 522 is a connection timeout to the origin. 523 means the origin is unreachable. 524 is Cloudflare's own gateway timeout (they waited 100 seconds by default). 525 means the SSL handshake with the origin failed. 527 is a Railgun error, which you'll likely never see unless you're using that specific Cloudflare feature.
Nginx 499 is the code that started this article. When a client closes the connection before Nginx finishes proxying the response, Nginx logs 499. It's not sent to anyone. It only exists in your access log. Common causes: the client had a short timeout, the user navigated away, or a mobile app went to background. A steady stream of 499s usually means your backend is too slow for your clients' patience.
AWS ALB behaviors add their own layer. ALB returns 460 when the client closes the connection before the load balancer can respond. It returns 502 when the target group has no healthy targets, which is a different cause than the standard 502 meaning. AWS also uses 503 when a target group has no registered targets at all. Checking ALB access logs requires looking at the elb_status_code and target_status_code fields separately to figure out where the error actually originated.
Decision Tree: Which Code Should I Return?
When you're building an API and need to pick the right status code, walk through this:
Did the request succeed?
├── Yes
│ ├── Did I create a new resource? → 201 Created
│ ├── Is there a response body?
│ │ ├── Yes → 200 OK
│ │ └── No → 204 No Content
│ └── Should the client look elsewhere? → 3xx (see redirect tree above)
└── No
├── Is it the client's fault?
│ ├── Can I not parse the request? → 400 Bad Request
│ ├── Is the request well-formed but semantically wrong? → 422 Unprocessable Content
│ ├── Do I not know who this is? → 401 Unauthorized
│ ├── Do I know who this is but they can't do this? → 403 Forbidden
│ ├── Does the resource not exist? → 404 Not Found
│ ├── Is the HTTP method wrong? → 405 Method Not Allowed
│ ├── Is the client sending too many requests? → 429 Too Many Requests
│ └── Is the request body too large? → 413 Content Too Large
└── Is it my fault?
├── Am I temporarily down? → 503 Service Unavailable
├── Did my upstream fail? → 502 Bad Gateway
├── Did my upstream time out? → 504 Gateway Timeout
└── Something else broke → 500 Internal Server Error Print this out. Tape it to your monitor. I'm serious. Picking the right status code makes debugging faster for everyone who consumes your API.
Frequently Asked Questions
What's the difference between 400 and 422?
400 Bad Request means the server can't parse the request at all. Broken JSON, missing Content-Type header, malformed URL encoding. 422 Unprocessable Content (renamed from "Unprocessable Entity" in RFC 9110) means the server parsed the request just fine, but the content doesn't make sense. An email field containing "abc123" is valid JSON but an invalid email. In practice, many APIs use 400 for both cases and include error details in the response body. If you want to be precise, use 422 for validation errors on well-formed requests.
Should I return 204 or 200 with an empty body?
Use 204 when you intentionally have no body to return. Use 200 with an empty body when... well, you probably shouldn't. A 200 with zero content confuses clients. If an endpoint returns data sometimes and nothing other times, consider returning 200 with {} or [] instead. A 204 is an explicit signal: "I processed this, and there is deliberately nothing to show you." DELETE and PUT operations without return values are the classic use case.
Is 418 I'm a teapot a real status code?
Sort of. It was defined in RFC 2324 as an April Fools' joke in 1998, part of the Hyper Text Coffee Pot Control Protocol (HTCPCP). It was never intended for real use. But it became so widely known that Google once returned 418 at google.com/teapot, and Node.js had it built into its HTTP module. When there was an effort to remove it from Node.js in 2017, the backlash was intense enough that it stayed. It's a fun piece of internet history, but please don't use it in production APIs. Your on-call engineers won't find it funny at 3 AM.
Do browsers and API clients handle status codes differently?
Yes, and the differences are significant. Browsers automatically follow 3xx redirects (up to a limit, usually 20 hops). API clients like curl don't follow redirects unless you pass -L. Browsers show a built-in error page for 4xx and 5xx responses if the server doesn't provide a custom one. API clients just return the status code and body as-is.
On 401 responses, browsers with an active session might redirect to a login page (via JavaScript or server-side logic). API clients expect the consuming code to handle it. Browsers also handle 304 Not Modified transparently; your JavaScript fetch() call receives what looks like a 200 with cached content. You never see the 304 in your code unless you check the network tab in DevTools.
Where can I find the full official list?
RFC 9110, Section 15 is the current standard (published June 2022, replacing RFC 7231). For a more readable version, MDN Web Docs' HTTP status code reference covers every code with examples and browser compatibility tables.
Wrapping Up
HTTP status codes are a shared vocabulary between clients and servers. Using them accurately means faster debugging, better API documentation, and fewer confused developers reading your logs. The five-class mental model gets you 80% of the way there. The decision tree handles the edge cases. And when you see a code in the 400-range that you don't recognize, check whether your CDN or load balancer is the one generating it before you start blaming your application code.
If you're working through networking fundamentals, the companion post on how DNS actually works covers what happens before any HTTP status code ever gets returned. For checking your response body character counts while building API error messages, the text counter is right here on the site.