410 Gone vs 404 Not Found (tested)

404 Not Found means the server has nothing at this URL right now. 410 Gone means the page existed and was removed on purpose, with no replacement. Both are errors to clients. The differences are small, but one of them surprised us.

1. The short answer

Use 410 when you removed a page on purpose and nothing replaces it. Use 404 for anything else that does not exist. If a replacement page exists, use a 301 redirect instead. See 301 vs 302 vs 307 vs 308.

SituationStatus
Typo URL, never existed404
Page deleted on purpose, no replacement410
Page moved or merged into another page301 to the new URL
Page still exists but should leave search results200 + noindex

For the last row, see X-Robots-Tag in nginx and Apache and how to check noindex.

2. Return 410 in plain Node.js

Keep a list of removed paths. Return 410 for those, 404 for everything else unknown. This is the server we tested:

import http from 'node:http';
const REMOVED = new Set(['/old-page', '/2019-sale']);

http.createServer((req, res) => {
  const path = new URL(req.url, 'http://x').pathname;
  if (REMOVED.has(path)) {
    res.writeHead(410, {'Content-Type': 'text/html; charset=utf-8'});
    return res.end('<h1>Gone</h1><p>This page was removed on purpose.</p>');
  }
  // ... your normal routes ...
  res.writeHead(404, {'Content-Type': 'text/html'});
  res.end('<h1>Not Found</h1>');
}).listen(8986);
$ curl -i http://127.0.0.1:8986/old-page
HTTP/1.1 410 Gone
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Transfer-Encoding: chunked

<h1>Gone</h1><p>This page was removed on purpose.</p>

In Express the same idea is one middleware placed before your routes. We did not run Express, but it uses the same Node response object:

const REMOVED = new Set(['/old-page', '/2019-sale']);
app.use((req, res, next) =>
  REMOVED.has(req.path) ? res.status(410).send('Gone') : next());

3. Return 410 in Flask

abort(410) works anywhere. A before_request hook handles a whole list:

from flask import Flask, abort, request
app = Flask(__name__)
REMOVED = {"/old-page", "/2019-sale"}

@app.before_request
def gone():
    if request.path in REMOVED:
        abort(410)
$ curl -i http://127.0.0.1:8987/old-page
HTTP/1.1 410 GONE
Server: Werkzeug/3.1.9 Python/3.13.5
Content-Type: text/html; charset=utf-8
Content-Length: 252
Connection: close

<!doctype html>
<html lang=en>
<title>410 Gone</title>
<h1>Gone</h1>
<p>The requested URL is no longer available on this server and there is no forwarding address. ...

Werkzeug writes the reason phrase as GONE in capitals. Clients ignore the phrase; only the number matters.

4. nginx and Apache

From the docs (not measured here). nginx:

location = /old-page { return 410; }

Apache (mod_alias):

Redirect gone /old-page

5. Caching: Chromium cached a 410 but not a 404

RFC 9111 says 404 and 410 are both "heuristically cacheable". That means a cache may store them even without Cache-Control, if it has a hint like an old Last-Modified. We tested this. The server sent Last-Modified: Mon, 01 Jan 2018 and no Cache-Control. The page called fetch() twice per URL in a fresh browser context. We counted requests at the server. We ran it twice; both runs gave the same counts.

ResponseHeadersServer hits for 2 fetchesCached?
200Last-Modified only1Yes (heuristic)
410Last-Modified only1Yes (heuristic)
404Last-Modified only2No
410Cache-Control: max-age=601Yes
404Cache-Control: max-age=601Yes

So Chromium 153 treats 410 like a 200 for heuristic caching, but not 404. This matters if you might bring a URL back. A browser can keep showing the cached 410 for a while. If you are not sure the removal is final, send Cache-Control: no-store or a short max-age with the 410. Or use 404.

6. What clients see

Client404410
curl --fail exit code2222
curl without --fail00
fetch() res.okfalsefalse
fetch() res.statusTextNot FoundGone
Chromium page.goto-status 410, HTML body rendered

curl --fail-with-body also exits 22, but prints the body first. fetch() does not throw on 404 or 410; you must check res.ok.

Chromium showed our custom 410 page normally. page.goto returned status 410 and the body text was "GoneThis page was removed on purpose." So give your 410 page real content: a short note and links to related pages.

7. curl --retry does not retry 404 or 410

curl --retry 3 only retries "transient" errors (timeouts and some 5xx codes, plus 408 and 429). We counted server hits:

$ curl -sS --retry 3 -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8986/h410
410          # 1 request, exit 0
$ curl -sS --retry 3 --fail -o /dev/null http://127.0.0.1:8986/old-page
curl: (22) The requested URL returned error: 410   # 1 request, exit 22
$ curl -sS --retry 3 --retry-all-errors --fail -o /dev/null http://127.0.0.1:8986/2019-sale
curl: (22) ... error: 410   (printed 4 times)       # 4 requests, exit 22

Plain --retry made one request for both 404 and 410. Adding --retry-all-errors with --fail retried the 410 three times. Avoid that combination in scripts that hit removed URLs.

8. Search engines

From Google's public statements, not measured here. Google's John Mueller has said a 410 can drop a URL from the index slightly faster than a 404, but both are fine in the long run. Google Search Console lists both under the "Not found (404)" page indexing report. That report is informational: removed pages returning 404 or 410 do not hurt the rest of your site. Fix links on your own site that still point to removed pages; see how to find broken links.

Frequently asked questions

Is 410 better than 404 for SEO?

Only slightly. Google has said 410 can drop a URL a little faster, but both lead to removal over time. Pick 410 when the removal is deliberate and final.

Should I redirect a deleted page instead of returning 410?

Yes, if a real replacement exists: use a 301 to the closest matching page. Do not redirect everything to the homepage; with no replacement, return 410 or 404.

Do browsers cache 410 responses?

In our test, Chromium 153 cached a 410 that had an old Last-Modified and no Cache-Control: two fetch() calls made one server request. A 404 with the same headers was not cached. With Cache-Control: max-age=60 both were cached.

Does curl --fail treat 404 and 410 the same?

Yes. Both gave exit code 22. Without --fail, curl exited 0 for both. fetch() returned ok=false for both and did not throw.

Can a 410 page have a normal HTML body?

Yes. Chromium rendered our custom 410 page, and Playwright's page.goto reported status 410. Add a short explanation and links so visitors are not stuck.

Written with AI assistance. Measured on 2026-10-02 with curl 8.14.1, Node.js 24.21.0 (plain http module), Flask on Werkzeug 3.1.9 and headless Chromium 153 via Playwright 1.63: we served 410 and 404 responses with and without Cache-Control and counted every request that reached the server. PlainHeaders is our extension; curl, Node.js, Flask and Chromium are free.