If a URL moved permanently, send 301. If it is gone and there is no useful replacement, send 410. Use 404 when the server cannot find a resource but you are not making that stronger, permanent claim.
The three codes are often treated as interchangeable because a browser shows an error page for two of them. They are not interchangeable to clients, crawlers, monitoring, or the person who later has to undo a redirect.
Pick the code from the URL's history
| What happened | Response | What the client should infer |
|---|---|---|
| The resource has a new canonical URL | 301 Moved Permanently + Location | Save and use the new URL |
| The resource was intentionally retired | 410 Gone | Stop expecting it to return |
| The route may be mistyped, unpublished, or unknown | 404 Not Found | There is no representation here right now |
301 is a redirect, not a tasteful replacement for an error page. Redirecting every old article to the homepage makes analytics noisy and gives a user looking for a deleted API reference no explanation. Redirect only to the closest real successor.
A
410is a commitment. Do not use it for a deployment race where a page might reappear after a cache purge; a404is safer for that uncertainty.
The redirect needs to preserve the request
For a simple page move, an absolute or relative Location works:
HTTP/1.1 301 Moved Permanently
Location: /journal/http-status-codes-404-410-301
Cache-Control: public, max-age=3600
Be careful with API endpoints. A redirect can change the method in older client behaviour and is usually the wrong migration path for a POST. Publish the new endpoint, deprecate the old one in the API contract, and return an explicit response body explaining the replacement.
Why a custom 404 still matters
A 404 page is for recovery: search, popular links, and a clear message that the URL itself failed. It must still return 404; serving a pretty page with 200 OK creates a soft 404 that confuses caches and search tooling.
For retired URLs with traffic, log the path before choosing between 301 and 410. Ten inbound links to a genuinely equivalent new guide is a redirect. Ten requests to an obsolete campaign URL with no equivalent is a 410 plus a useful retirement note.
A small deployment checklist
- Test the status and
Location, not just the rendered page:curl -I URL. - Keep query parameters only when the destination can make sense of them.
- Remove redirect chains;
old → older → newwastes a request and hides mistakes. - Monitor 404s after release. They are often the first evidence of a broken internal link.
Correct status codes are boring infrastructure. That is exactly the goal: a user, bot, and API client should all get the same unambiguous answer.
