410 Gone
Client error response defined by RFC 9110 §15.5.11.
Last updated July 27, 2026
Common causes at a glance
- A time-limited campaign or promotional page retired on purpose
- An API version sunset after its deprecation window closed
- User-deleted content that must not be resurrected or redirected
- A tombstone written by a CMS when an author unpublishes permanently
Reproduce it
curl -i https://www.example.com/promo/spring-2019What 410 Gone means
The difference between 404 and 410 is knowledge, not severity. 404 says “no current representation, no comment on why”; 410 says “this was intentional and permanent”. A server should only send 410 when it knows that, which means recording a deletion rather than merely failing to find something.
The payoff is on the consumer side. Crawlers treat 410 as a stronger signal and drop the URL sooner, without the grace period they apply to 404s in case the absence is a glitch. Feed readers, link checkers, and API clients can prune rather than retrying on a schedule.
What the spec says
RFC 9110 §15.5.11 says access is no longer available and the condition is likely permanent, and — importantly — that a server which does not know whether the condition is permanent ought to use 404 instead. The stated purpose is web maintenance: telling recipients the resource is intentionally unavailable and that remote links should be removed. It also says there is no need to mark every removed resource as gone, nor to keep the mark indefinitely. Like 404, a 410 is heuristically cacheable (RFC 9111 §4.2.2), which cuts both ways: it stops repeat traffic, and an accidental 410 outlives the deploy that caused it.
What actually causes it
410 essentially never happens by accident, which makes it the most informative code in this range: someone configured it. The usual sources are campaign pages with an end date, sunset API versions where a 410 pointing at the successor is kinder than a silent 404, and deleted user content with no equivalent replacement to redirect to.
The mistake is using it as a blunt cleanup tool during a migration. 410 on URLs that have a valid successor throws away the ranking signal a 301 would carry, and since 410 is cacheable and crawlers act quickly, reversing it is slow. The opposite error is leaving retired URLs on 404 forever, so crawlers keep requesting them for months.
How to debug it
Confirm the 410 is intentional by finding the rule that produces it. Check the server or CDN config for an explicit mapping before assuming an application bug; a 410 with no matching rule is almost always a CMS tombstone. Audit a list of retired URLs in one pass to check the classification is what you meant:
while read -r u; do
printf '%s %s\n' "$(curl -o /dev/null -s -w '%{http_code}' "$u")" "$u"
done < retired-urls.txt
Anything showing 404 that is permanently gone should become 410; anything showing 410 with
a real successor should become a 301. For a sunset API, include a
Link header
pointing at the replacement, since clients hitting a 410 have no other way to find it. An
over-long Cache-Control on a mistaken 410 keeps serving after the rollback.
Working examples
curl
# Is it 410, 404, or a redirect? Sweep a list of retired URLs.
while read -r u; do
printf '%s %s\n' "$(curl -o /dev/null -s -w '%{http_code}' "$u")" "$u"
done < retired-urls.txt
# A sunset API should point at its successor.
curl -sD - -o /dev/null https://api.example.com/v1/items \
| grep -iE '^(HTTP/|link|sunset|deprecation|cache-control)'
# Check how long the 410 will stick around in caches.
curl -sI https://www.example.com/promo/spring-2019 | grep -i cache-control
Python (requests)
import requests
urls = [
"https://www.example.com/promo/spring-2019",
"https://www.example.com/moved-page",
]
for u in urls:
r = requests.head(u, allow_redirects=False, timeout=10)
verdict = {
410: "gone on purpose",
404: "consider 410 if permanent",
301: "redirected, link equity preserved",
}.get(r.status_code, "check this")
print(f"{r.status_code} {verdict:34} {u}")
if r.status_code == 410:
print(" cache-control:", r.headers.get("Cache-Control"))
Node (fetch)
const retired = [
"https://www.example.com/promo/spring-2019",
"https://api.example.com/v1/items",
];
for (const url of retired) {
const res = await fetch(url, { method: "HEAD", redirect: "manual" });
console.log(res.status, url);
if (res.status === 410) {
// A sunset endpoint should still tell clients where to go next.
console.log(" link:", res.headers.get("link"));
console.log(" cache-control:", res.headers.get("cache-control"));
}
}Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request