Skip to content
Control Plane Labs

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-2019

What 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