Network

HTTP Status Codes as a Debugging Compass: What Each Class Really Tells You

· 4 min read

The fastest debugging tool in web development is three digits long. Before you read a response body, before you open a log, the status code has already told you where to look: whose fault it probably is, whether retrying can help, and what layer produced the failure. Developers who internalize the taxonomy debug visibly faster than those who treat codes as folklore ('404 means not found, 500 means bad').

The classes carry the core signal: 2xx success, 3xx redirection, 4xx the client did something wrong, 5xx the server failed. Everything after that is precision — and a handful of codes people persistently confuse.

401 vs 403: who are you vs no, you can't

401 Unauthorized is a naming accident — it means unauthenticated: the server does not know who you are (missing, expired, or invalid credentials), and a fresh login/token can fix it. 403 Forbidden means authentication succeeded but authorization failed: the server knows exactly who you are and the answer is no. Retrying a 403 with the same identity is pointless; that's a permissions conversation.

This distinction is a debugging fork: 401 → inspect the credential (is the token present? expired? sent in the right header?); 403 → inspect the policy (roles, scopes, resource ownership, IP allowlists). For your own APIs, one security nuance: returning 404 instead of 403 for resources the caller shouldn't know exist is a legitimate pattern (GitHub does it for private repos) — just apply it consistently.

The redirect family: 301, 302, 307, 308

Two axes distinguish the four: permanence (does the client cache the new location?) and method preservation (does a POST stay a POST?). 301 is permanent, 302 temporary — and both, for historical reasons, allow clients to downgrade POST to GET, which drops the request body. Their modern strict siblings 308 (permanent) and 307 (temporary) forbid that method change.

Two production consequences. First: API endpoints that redirect POSTs should use 307/308, or clients will mysteriously arrive as body-less GETs. Second: 301s are cached aggressively by browsers, often for months, with no practical purge — shipping a wrong 301 is a long-lived mistake. Use 302 while validating a migration, then flip to 301. And watch for redirect chains in curl -v: each hop costs a round trip, and HTTPS→HTTP hops silently drop Authorization headers.

5xx forensics: 500, 502, 503, 504 point at different layers

In modern deployments a proxy or load balancer fronts your application, and the gateway codes identify which side of that boundary failed:

  • 500 Internal Server Error — your application code threw. Stack trace is in your app logs. The only one of these four that is unambiguously your bug.
  • 502 Bad Gateway — the proxy reached out and got garbage or nothing: app crashed, wrong upstream port, app restarting. Check whether the process is alive before reading any logs.
  • 503 Service Unavailable — deliberate refusal: overload, maintenance mode, failing health checks, no healthy instances behind the balancer. Often accompanied by Retry-After.
  • 504 Gateway Timeout — the app is alive but slower than the proxy's patience: slow queries, upstream API hangs, thread-pool exhaustion. Compare app response times to the proxy timeout setting.

The codes that encode etiquette: 429, 405, 409, 422

429 Too Many Requests is the rate limiter speaking; well-behaved clients honor Retry-After and back off exponentially with jitter — hammering a 429 is how retry storms take services down. 405 Method Not Allowed usually means a routing or trailing-slash mismatch rather than a missing endpoint (the URL exists; your verb doesn't). 409 Conflict is the concurrency signal — version mismatches, duplicate creation attempts — and generally means 'refetch state, then retry the operation'. 422 Unprocessable Entity, imported from WebDAV into REST convention, distinguishes 'your JSON parsed but fails validation' from 400's 'I couldn't even parse it' — a distinction your API consumers will thank you for.

Choosing codes for your own API

Design principles that survive contact with clients: never return 200 with {"error": …} in the body — it defeats monitoring, caching, and retry logic simultaneously, and it is the single most common status-code sin in the wild. Distinguish auth failures precisely (401 vs 403). Return 201 with a Location header for creations, 204 for successful deletions and empty updates. Reserve 400 for malformed requests and 422 for semantic validation failures. Send Retry-After with 429 and 503. And keep the mapping consistent — clients build retry and alerting logic on your codes, so an inconsistent API exports its confusion to every consumer.

When debugging any API, two commands show the truth: curl -i (status plus headers) and curl -v (the full conversation including redirects). Read the code first; it names the layer. Then read the headers; they carry the metadata (Retry-After, Location, WWW-Authenticate) that turns the three digits into a diagnosis.

← All guides