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.
| Code | Name | When you see it |
|---|---|---|
| 200 | OK | The normal case. SEO: also the code behind soft 404s |
| 201 | Created | A POST or PUT made a new resource (often with Location) |
| 202 | Accepted | Queued for later processing, not done yet |
| 204 | No Content | Success with an empty body. SEO: Google gets nothing to index |
| 206 | Partial Content | A Range request (video seeking, resumable downloads) |
3xx Redirection.
| Code | Name | Method kept? | Google treats it as |
|---|---|---|---|
| 301 | Moved Permanently | No, POST may become GET | SEO: permanent, strong signal |
| 302 | Found | No, POST may become GET | SEO: temporary, weak signal |
| 303 | See Other | Becomes GET (or HEAD) | SEO: temporary |
| 304 | Not Modified | n/a (cache revalidation) | Content unchanged since the last crawl |
| 307 | Temporary Redirect | Yes | SEO: same as 302 |
| 308 | Permanent Redirect | Yes | SEO: same as 301 |
4xx Client error.
| Code | Name | When you see it |
|---|---|---|
| 400 | Bad Request | Malformed syntax, bad framing, invalid JSON |
| 401 | Unauthorized | No valid credentials (see below) |
| 403 | Forbidden | Credentials fine or irrelevant, still refused |
| 404 | Not Found | SEO: nothing here; indexed URLs get dropped |
| 405 | Method Not Allowed | Wrong verb. Must include an Allow header |
| 408 | Request Timeout | Client took too long to send the request |
| 409 | Conflict | State clash, like a stale version on PUT |
| 410 | Gone | SEO: removed on purpose, permanently |
| 413 | Content Too Large | Upload over the server’s limit (testing upload limits) |
| 415 | Unsupported Media Type | Wrong Content-Type for the endpoint |
| 422 | Unprocessable Content | Valid syntax, invalid meaning (validation failed) |
| 429 | Too Many Requests | SEO: 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.
| Code | Name | When you see it |
|---|---|---|
| 500 | Internal Server Error | Unhandled exception, the generic catch-all |
| 501 | Not Implemented | Server doesn’t support the method at all |
| 502 | Bad Gateway | A proxy (nginx, load balancer, CDN) got an invalid response from upstream |
| 503 | Service Unavailable | SEO: overload or maintenance, temporary |
| 504 | Gateway Timeout | A 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 see | Check first |
|---|---|
| 301 where you expected 200 | Trailing slash, http vs https, www rules; follow the chain |
| 302 on a permanent move | Host or framework default; change it to 301 or 308 |
| POST arrives as GET after a redirect | It was a 301/302/303; use 307/308 |
ERR_TOO_MANY_REDIRECTS | A loop, often HTTPS forced at both the CDN and the origin |
| 304 when you changed the file | ETag/Last-Modified didn’t change; cache headers |
| 400 on a JSON API | Body syntax and the Content-Type header |
| 401 | Missing or expired token; read the WWW-Authenticate header |
| 403 | Permissions, WAF or bot rules, file ownership on static hosts |
| 404 on a page that exists | Case-sensitive paths, missing rewrite rule, deploy didn’t include it |
| 405 | The Allow header lists the verbs that work |
| 413 | Upload limit in the proxy (client_max_body_size in nginx) or the app |
| 415 | Content-Type sent vs the type the endpoint expects |
| 429 | Retry-After value; whether your client backs off at all |
| 500 | Application logs. The status alone tells you nothing |
| 502 | Is the upstream process running and listening on the right port |
| 503 | Planned maintenance, overload, or a health check failing |
| 504 | Slow upstream query or a proxy timeout set too low |
| 200 on an error page | Soft 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.