410 Gone vs 404: what to return for a retired API endpoint
Return 410 Gone, not 404 Not Found, when you retire an API endpoint
or version. A 410 tells the caller the endpoint existed, was removed on purpose and won't
come back; a 404 sends them hunting for a typo. Include a problem-details body that names the
retirement date and links to the migration guide.
What does each status tell the caller?
410 says "removed on purpose", 404 says "nothing here", and a redirect says "it moved". Pick the one that matches what happened:
| Status | What the caller learns | Use it when |
|---|---|---|
410 Gone |
This existed, and is permanently gone. | A version or endpoint you retired on purpose. |
404 Not Found |
Nothing here, and no reason given. | A path that never existed, or one you won't confirm. |
301 / 308 |
It moved. Follow the Location header. |
The replacement accepts exactly the same request. |
200 + Sunset |
Still works, but not for long. | Before the retirement date. |
RFC 9110 defines 410 as a resource that is no longer available, where the condition is likely to be permanent. That's a retired API version exactly.
Be careful with redirects. A 308
to v2
only works if v2
accepts the same request body and returns a shape the caller can parse. Usually it doesn't,
which is why you made a new version.
What should a 410 response body say?
It should say what was retired, when, and where the replacement is, so the last straggler can fix their integration without opening a ticket. Use
the problem details format from RFC 9457, and keep the
Link
header you sent during deprecation:
HTTP/1.1 410 Gone
Content-Type: application/problem+json
Link: <https://api.example.com/docs/migrate-to-v2>; rel="deprecation"; type="text/html"
{
"type": "https://api.example.com/problems/version-retired",
"title": "API version retired",
"status": 410,
"detail": "v1 was retired on 30 June 2027. Use v2 instead.",
"migration_guide": "https://api.example.com/docs/migrate-to-v2"
}
Keep the type
URL stable and document it, so a client can match on it in code. A human reads detail; a program reads
type
and status. Leave out anything that changes per request, such as a timestamp,
so the response can be cached and compared.
Is 404 ever the right answer for a retired endpoint?
Yes, in two cases. The first is a path that only ever existed for one customer, or that would reveal something you don't want to confirm, such as an internal admin route: there, 404 tells a stranger nothing. The second is an endpoint that never had real traffic, where no one will read the 410 anyway.
Everywhere else, 410 is the kinder answer. A caller who gets a 404 for an endpoint they used yesterday checks their base URL, their environment and their credentials before they suspect a retirement. A caller who gets a 410 with a migration link fixes the right thing first.
What about 400, 426 or 501?
None of them fits as well. 400 Bad Request
blames the caller's request, when the request is fine and the endpoint is what changed.
426 Upgrade Required
is about switching protocols, such as moving to TLS or a newer HTTP version, not API
versions. 501 Not Implemented
says the server does not support the method at all, which reads as a bug on your side.
How do you return 410 for a whole retired version?
Catch every path under the retired prefix with one handler, placed after your live routes so it never shadows them.
Express:
app.use("/v1", (req, res) => {
res
.status(410)
.set("Link", '<https://api.example.com/docs/migrate-to-v2>; rel="deprecation"; type="text/html"')
.type("application/problem+json")
.send(JSON.stringify({
type: "https://api.example.com/problems/version-retired",
title: "API version retired",
status: 410,
detail: "v1 was retired on 30 June 2027. Use v2 instead.",
}));
});
Phoenix:
# router.ex, after the live scopes
scope "/v1", MyAppWeb do
match :*, "/*path", RetiredController, :gone
end
# retired_controller.ex
defmodule MyAppWeb.RetiredController do
use MyAppWeb, :controller
def gone(conn, _params) do
conn
|> put_resp_header("link", ~s(<https://api.example.com/docs/migrate-to-v2>; rel="deprecation"; type="text/html"))
|> put_resp_content_type("application/problem+json")
|> send_resp(410, Jason.encode!(%{
type: "https://api.example.com/problems/version-retired",
title: "API version retired",
status: 410,
detail: "v1 was retired on 30 June 2027. Use v2 instead."
}))
end
end
Can you return 410 for one endpoint in a version that still works?
Yes. Retiring a single endpoint, such as an old export route, inside a version that stays
live is common, and 410 says exactly that: this one route is gone, the rest still works.
Give that route its own handler, its own migration link, and its own Sunset
date during the notice period, so clients can tell it apart from the version as a whole.
Watch the methods too. If GET /v1/reports
stays but POST /v1/reports
is retired, return 410 for the POST only. A 405 Method Not Allowed
describes a method the resource doesn't support, and says nothing about a retirement or
where to go next.
How should a client handle a 410?
Stop retrying and raise it to a person. A 410 is permanent, so a retry loop only adds load and hides the real problem in the logs. If you publish an SDK, make it treat 410 as a non-retryable error that names the migration link from the response, so the developer sees the fix in the exception message.
- Don't put 410 in the list of statuses your HTTP client retries, the way 502 and 503 often are.
- Alert on it: one 410 in production means a call path nobody migrated.
- Log the
typeanddetailfields, not only the status code.
How long should you keep returning 410?
At least as long as your notice period was. The 410 handler costs almost nothing to run, and every request it answers is a caller you can still reach. Log those requests by client: the list is your follow-up list.
One caution: caches may store a 410 by default, so don't send one for something you might bring back. If you're not sure the retirement is final, you're not ready to retire.
Sources
- RFC 9110, HTTP Semantics, which defines 410 and 404.
- RFC 9457, Problem Details for HTTP APIs, the body format above.
- IANA HTTP Status Code Registry.
- MDN: 410 Gone and MDN: 404 Not Found.
Where EndPointBlank fits
EndPointBlank tracks each endpoint version from its first deploy to its retirement. A version that leaves production after being deprecated is marked retired automatically. If a version marked retired is still being served in production, the portal flags it, because callers of that version are getting no warning at all.
See how version lifecycle works or start free. For the whole process, read how to deprecate an API version.