🍱 Lunchbox Hands

http

HTTP Status Codes: The Reference You Actually Use, With the SEO Ones Flagged

The HTTP status codes developers actually meet, by class, plus the confusing pairs: 301 vs 308, 404 vs 410, 401 vs 403, 429, 503 and soft 404s.

The IANA registry has about 60 assigned HTTP status codes, and most lists online give every one of them equal weight. You meet about 30 of them in real work, and the ones that cost you money are the ones that look alike: 301 vs 308, 404 vs 410, 401 vs 403, and a 200 that is secretly an error.

Semantics come from RFC 9110 (current HTTP semantics), RFC 6585 for 429, and the IANA registry for names. Google behavior comes from Google Search Central. Codes marked SEO are ones where Googlebot’s handling changes what gets indexed.

The short list, by class

The first digit is the class. A client that doesn’t recognize a code is supposed to treat it like the x00 of its class, so an unknown 4xx is handled like a 400.

1xx Informational. Rare in app code. You will mostly see 101 Switching Protocols (a WebSocket handshake succeeded) and 103 Early Hints (preload headers sent before the real response).

2xx Success.

CodeNameWhen you see it
200OKThe normal case. SEO: also the code behind soft 404s
201CreatedA POST or PUT made a new resource (often with Location)
202AcceptedQueued for later processing, not done yet
204No ContentSuccess with an empty body. SEO: Google gets nothing to index
206Partial ContentA Range request (video seeking, resumable downloads)

3xx Redirection.

CodeNameMethod kept?Google treats it as
301Moved PermanentlyNo, POST may become GETSEO: permanent, strong signal
302FoundNo, POST may become GETSEO: temporary, weak signal
303See OtherBecomes GET (or HEAD)SEO: temporary
304Not Modifiedn/a (cache revalidation)Content unchanged since the last crawl
307Temporary RedirectYesSEO: same as 302
308Permanent RedirectYesSEO: same as 301

4xx Client error.

CodeNameWhen you see it
400Bad RequestMalformed syntax, bad framing, invalid JSON
401UnauthorizedNo valid credentials (see below)
403ForbiddenCredentials fine or irrelevant, still refused
404Not FoundSEO: nothing here; indexed URLs get dropped
405Method Not AllowedWrong verb. Must include an Allow header
408Request TimeoutClient took too long to send the request
409ConflictState clash, like a stale version on PUT
410GoneSEO: removed on purpose, permanently
413Content Too LargeUpload over the server’s limit (testing upload limits)
415Unsupported Media TypeWrong Content-Type for the endpoint
422Unprocessable ContentValid syntax, invalid meaning (validation failed)
429Too Many RequestsSEO: rate limited; Googlebot slows down

Two of those got renamed in RFC 9110. 413 was “Payload Too Large” and 422 was “Unprocessable Entity”. Older docs and libraries still use the old names. Same codes, same meaning.

5xx Server error.

CodeNameWhen you see it
500Internal Server ErrorUnhandled exception, the generic catch-all
501Not ImplementedServer doesn’t support the method at all
502Bad GatewayA proxy (nginx, load balancer, CDN) got an invalid response from upstream
503Service UnavailableSEO: overload or maintenance, temporary
504Gateway TimeoutA proxy got no timely response from upstream

About 418 “I’m a teapot”: it’s not a real status code. It came from an April 1 RFC (2324). RFC 9110 lists it as “418 (Unused)” and reserves it only because the joke got deployed often enough that the number can’t be safely reused. Don’t return it from a real API.

301 vs 302 vs 307 vs 308

Each code answers two separate questions.

  • Permanent or temporary? 301 and 308 are permanent. 302 and 307 are temporary.
  • Does the method survive? 307 and 308 must keep it. RFC 9110 says a client following a 307 “MUST NOT change the request method.” 301 and 302 allow a POST to turn into a GET “for historical reasons”, and browsers do exactly that. The Fetch standard tells browsers to switch a POST to GET (and drop the body) on 301 or 302, and to switch anything except GET or HEAD to GET on 303.

For Google, the permanent/temporary split is what matters. Its redirect docs list 301 and 308 as permanent: the target is used as a signal for the canonical URL. 302, 303 and 307 are temporary: Googlebot follows them but the indexing pipeline doesn’t treat the target as canonical. Its HTTP errors doc says crawlers follow up to 10 redirect hops by default.

So the rule is short. Pages that moved for good get a 301 (or a 308, which Google treats the same). API endpoints that receive POST or PUT get a 308 or 307 so the body isn’t silently lost. Chains, stack defaults and config snippets are in 301 vs 302 redirects, and the nginx side is in return vs rewrite.

404 vs 410

RFC 9110 draws the line on knowledge. 404 says nothing is here and makes no claim about whether that’s temporary. 410 says it’s gone and “likely to be permanent”, and is “preferred over 404 if the origin server knows” that. If you aren’t sure, the spec says to use 404.

For Google, the difference is small. Its HTTP errors doc says Google doesn’t use content from any 4xx URL, already indexed 4xx URLs are removed, and all 4xx codes except 429 “are treated the same.” Its soft 404 guidance lists “404 (not found) or 410 (gone)” side by side as the right answer for a removed page. Use 410 when you deleted something on purpose and want that on record. Don’t expect an SEO difference.

401 vs 403

The names are misleading. 401 “Unauthorized” is really about authentication: you haven’t proven who you are. A 401 must carry a WWW-Authenticate header with at least one challenge. If you sent credentials, a 401 means they were rejected.

403 “Forbidden” is about permission: the server understood the request and won’t do it. Sending the same credentials again won’t help. A server can also return 404 instead of 403 to hide that the resource exists, and RFC 9110 allows that.

Quick test: would logging in fix it? Then it’s a 401. Is the user logged in and still not allowed? Then it’s a 403.

SEO note: Google says 4xx codes other than 429 have no effect on crawl rate, so 401 or 403 won’t throttle Googlebot. They just get URLs dropped.

429 and Retry-After

429 is from RFC 6585: “too many requests in a given amount of time.” The response MAY include Retry-After, and a cache MUST NOT store it. Retry-After takes either seconds or an HTTP date:

HTTP/1.1 429 Too Many Requests
Retry-After: 120
Content-Type: application/json

{"error": "rate_limited", "retry_after_seconds": 120}

Clients should honor it and back off. Google’s crawlers treat 429 as a signal that the server is overloaded and slow down, the same way they react to 5xx. If Googlebot is hitting you too hard, 429 or 503 is the correct throttle. 403 isn’t.

503 for maintenance

RFC 9110 defines 503 as temporary overload “or scheduled maintenance” and says the server MAY send Retry-After to say how long. Google’s guidance on pausing a site matches that: if you need to take the site down for 1–2 days, return an informational error page with a 503, and use Retry-After “with a best effort date or duration.” Taking the whole site down should only last “a few days at most.”

HTTP/1.1 503 Service Unavailable
Retry-After: Sat, 17 Oct 2026 06:00:00 GMT
Content-Type: text/html

Two traps from the same Google page. Never return 503 for robots.txt, because that blocks all crawling. And for longer closures, Google suggests keeping an indexable home page up with a 200 rather than leaving a 503 in place. The worst option is a maintenance page served as 200 everywhere. Every URL then tells Google its content is “down for maintenance.”

The soft 404: a 200 that lies

A soft 404 is a page that says “not found” but returns 200. Google’s crawl troubleshooting doc defines it that way, and Search Console reports it as a separate error. Common causes:

  • A custom error template that the framework renders with 200.
  • A single-page app that shows its “not found” view on the client while the server returned the app shell with 200.
  • Empty pages, like an internal search or category page with zero results (Google names this one).

Google’s fix is clear. Gone with no replacement: return 404 or 410. Moved: 301 to the equivalent page. Still there but flagged: check what Googlebot actually renders with URL Inspection, because failed scripts or resources can make a real page look empty.

If you see X, check Y

You seeCheck first
301 where you expected 200Trailing slash, http vs https, www rules; follow the chain
302 on a permanent moveHost or framework default; change it to 301 or 308
POST arrives as GET after a redirectIt was a 301/302/303; use 307/308
ERR_TOO_MANY_REDIRECTSA loop, often HTTPS forced at both the CDN and the origin
304 when you changed the fileETag/Last-Modified didn’t change; cache headers
400 on a JSON APIBody syntax and the Content-Type header
401Missing or expired token; read the WWW-Authenticate header
403Permissions, WAF or bot rules, file ownership on static hosts
404 on a page that existsCase-sensitive paths, missing rewrite rule, deploy didn’t include it
405The Allow header lists the verbs that work
413Upload limit in the proxy (client_max_body_size in nginx) or the app
415Content-Type sent vs the type the endpoint expects
429Retry-After value; whether your client backs off at all
500Application logs. The status alone tells you nothing
502Is the upstream process running and listening on the right port
503Planned maintenance, overload, or a health check failing
504Slow upstream query or a proxy timeout set too low
200 on an error pageSoft 404; make the server return the real code

The browser can mislead you here: a 301 may come from its cache, a 307 may be its own HSTS upgrade, and CORS errors hide the real status from your script (see why curl works and fetch doesn’t). Check from outside the browser (reading curl flags helps).

Checking a real response

To see every hop of a redirect with its status, run the URL through the Redirect Checker. It lists each URL in the chain with its code and flags long chains and mixed HTTP/HTTPS hops. The HTTP Header Analyzer prints the full response header set (Location, Retry-After, Cache-Control, ETag) and scores the security headers. For an API where the method and body matter, the HTTP Request Builder sends GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS with your own headers and body, and shows the status, response headers, body and timing. If a code turns up that you don’t recognize, search it by number or name in the HTTP Status Code Reference, which lists the standard IANA codes under their RFC 9110 names.