Broken Link Checker - Dead Links, 404 Checker, Bulk URL Status
Pricing
from $2.22 / 1,000 link checkeds
Broken Link Checker - Dead Links, 404 Checker, Bulk URL Status
For SEO audits, content teams and docs maintainers: checks every link on a page and reports the state each is actually in - ok, not-found, blocked-to-us, server-error or unreachable. A 403 is not a dead link: on 2026-08-26, 6 of 15 well-known sites answered 403 while working fine in a browser.
Pricing
from $2.22 / 1,000 link checkeds
Rating
0.0
(0)
Developer
NeverEmpty
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
1
Monthly active users
7 days ago
Last modified
Categories
Share
For site migrations, content audits, affiliate-link maintenance and SEO housekeeping: checks every link on a page and reports which state each one is actually in — alive, genuinely gone, or merely refusing automated requests.
A naive checker would have told you 12 of 15 links were broken. One of them was. Measured on 26 August 2026 against fifteen well-known sites with a bot user-agent: 6 of 15 answered 403 to the checker (stackoverflow.com, yelp.com, tripadvisor.com, etsy.com, quora.com, glassdoor.com) — refusing automation, not broken. 2 of 15 refused HEAD but answered GET (amazon.com HEAD 405 → GET 202; medium.com HEAD 403 → GET 200). 4 of 15 answered with a 2xx that is not 200 (202, 206). 1 of 15 was genuinely gone.
So 403 gets a category of its own (blocked-to-us), separate from not-found, because a person clicking that link in a browser will very likely reach the page. Every row also carries naiveCheckWouldSayBroken — true when a HEAD-and-200-only check would have called this link broken while this checker found it alive or merely blocked. That column is the difference between the two tools, stated per row.
Nothing here manufactures the result it is measuring. Requests to one server are limited (two at a time by default), because firing everything at once makes a server answer 429 — and 429 is blocked-to-us. With no input at all it checks https://www.w3.org/History.html, a page whose outbound links have had thirty years to rot: measured on Apify on 11 September 2026, 67 links, including http://europa.eu.int/comm/index.htm and http://www.cern.ch/WWW94/, whose domains no longer resolve.
No API key. Export as JSON, CSV or Excel.
Why that distinction is the whole product
Measured on 26 August 2026 against fifteen well-known sites, using a bot user-agent:
| What happened | How many | Examples |
|---|---|---|
Answered 403 to the checker — not broken, just refusing automated requests | 6 of 15 | stackoverflow.com, yelp.com, tripadvisor.com, etsy.com, quora.com, glassdoor.com |
Refused HEAD but answered GET | 2 of 15 | amazon.com (HEAD 405 → GET 202), medium.com (HEAD 403 → GET 200) |
Answered with a 2xx that is not 200 | 4 of 15 | amazon.com and imdb.com and booking.com (202), github.com (206) |
| Genuinely gone | 1 of 15 | a 404 URL used as a control |
A checker that sends HEAD and treats "not 200" as broken would have reported twelve of those fifteen links as broken. One of them actually was.
This checker sends HEAD, retries with a short ranged GET when the server rejects HEAD, accepts every 2xx, and keeps 403 in a category of its own.
The states
state | Meaning |
|---|---|
ok | Any 2xx, after redirects. 202 and 206 count. |
not-found | 404 or 410. The link is genuinely gone. |
blocked-to-us | 401, 403, 429, 451. The server refused this checker. This is not a broken link. |
server-error | 5xx. Often temporary; retried, and the state reflects the last attempt. |
client-error | Another 4xx, a 3xx with no Location, or a redirect loop. |
unreachable | No HTTP answer at all — DNS has no record, connection refused, TLS failed, or the request timed out. The reason says which. |
Every row also carries naiveCheckWouldSayBroken: true when a HEAD-and-200-only check would have called this link broken, while this checker found it alive or merely blocked. That column is the difference between the two tools, stated per link.
What it does not claim
Attached to every row as notClaimed:
- A
blocked-to-uslink is not a broken link. The server refused this checker specifically. - Page content is not inspected. A URL that returns
200with the words "page not found" written in the body is reported asok. - JavaScript is not executed, so links created by scripts after load are not seen.
- A
server-errorcan be temporary; it is retried, and the state reflects the last attempt.
A real run, with the default input
When no input is given at all, the example page https://www.w3.org/History.html is checked — a page about the early history of the web, whose outbound links have had thirty years to rot. Measured on Apify on 11 September 2026: 67 links in about 25 seconds — 51 ok, 10 not-found, 1 blocked-to-us, 5 unreachable.
The same run from a home connection took 50 seconds and reported one more link as unreachable. That is not a discrepancy to hide: reachability is measured from wherever the check runs, and a host that times out from one network can answer from another. The row says unreachable with the transport reason rather than not-found, precisely so the difference is visible.
Among them: http://europa.eu.int/comm/index.htm (the domain no longer resolves), http://www.cern.ch/WWW94/ (same), and several W3C pages that now return 404. One link, http://www.acm.org/, answers 403 to automated checks and would have been reported as broken by a naive check; here it is blocked-to-us.
Input
| Field | Type | Default | What it does |
|---|---|---|---|
startUrls | array | none | Pages whose links should be checked. Each distinct link is checked once, no matter how many pages point at it, and each page is read once. A link to a file (requestsFromUrl) also works: an XML sitemap is read from its <loc> entries (for a sitemap index, up to 20 of its sitemaps), and any other text file is read for every http(s) URL in it. |
urls | array | empty | Check these URLs directly, without reading a page first. Can be combined with startUrls. Write the full address with http:// or https://. |
checkInternal | boolean | true | Include links back to the same site. The www prefix is ignored when deciding. |
checkExternal | boolean | true | Include links to other sites. |
maxLinks | number | 100 | How many links to check. Taken in page order; the run reports how many were left unchecked rather than hiding the truncation. Once enough links are found, the remaining pages are not read, and a row says where reading stopped. |
concurrency | number | 5 | Links checked in parallel across all servers. |
perHostConcurrency | number | 2 | Never more than this many at once to one server. |
respectRobots | boolean | true | Applies to fetching the pages links are read from. |
timeoutSecs | number | 20 | Timeout for one link. Range 3–120. |
maxRetries | number | 3 | Retries for 429 and 5xx, both for the links and for the pages they are read from. Range 1–10. |
The example page https://www.w3.org/History.html is checked only when no pages and no links are given. If you give only urls, only those are checked and charged. The input form opens with the example page already filled in; if you leave it there and add your own links in urls or a requestsFromUrl file, the example page is skipped, not charged, and a row says so.
Two deliberate choices about speed
Requests to one server are limited. Sending everything at once makes a server answer 429, and 429 is blocked-to-us — the checker would be manufacturing the very result it is measuring. Default is two at a time per server, five in total.
Timeouts are retried at most twice, not maxRetries times. A timeout costs the entire timeout period, and a server that did not answer in twenty seconds is rarely fixed by asking again six hundred milliseconds later. On the default page one dead host was taking 21 seconds per attempt; three attempts made the whole run 74 seconds. Capping timeout attempts at two brought it to 50 seconds with identical results. 429 and 5xx still get the full maxRetries, because those genuinely do clear.
What each row contains
url, finalUrl (after redirects), state, httpStatus, reason, naiveCheckWouldSayBroken, method (whether HEAD was enough or GET was needed), headRejectedStatus, redirectCount, redirectChain (every hop with its status), elapsedMs, transportError, isInternal, linkedFrom (which pages link to it), anchorTexts, notClaimed, checkedAt.
Pages that could not be read for links are returned too, with state: null and a reason — the page answered an HTTP error such as 403 (shown in httpStatus), robots.txt disallowed it, it was not HTML (a PDF, or a sitemap given as a page instead of as a requestsFromUrl file), it had no links, or every link was left out by checkInternal / checkExternal. Inputs that could not be used get the same kind of row: a URL without http:// or https://, an empty input, or a URL list that could not be downloaded. Those rows are not charged.
A run where no link could be checked still ends as succeeded, charges nothing, and says why in those rows and in the run's status message.
Typical uses
- Link rot on old content. Long-lived pages accumulate dead outbound links; this separates the genuinely dead from the merely unfriendly-to-bots.
- Before a site migration. Record the redirect chain of every internal link, then compare afterwards.
- Cleaning a directory or resource list.
not-foundandunreachableare actionable;blocked-to-ususually is not, and should not be deleted by mistake.
FAQ
Why is 403 not treated as broken?
Because six of the fifteen sites measured answer 403 to any automated checker while working perfectly in a browser. Deleting those links would remove working references.
Why send HEAD first?
It avoids downloading page bodies. When a server rejects HEAD — amazon.com answers 405, medium.com answers 403 — the check is repeated with a GET limited to the first 2 KB.
Does it follow redirects? Yes, and it records them. Redirects are followed manually rather than by the HTTP client, because an automatically-followed redirect is invisible afterwards and the chain is often the thing you need to see.
Is a 200 guaranteed to mean the page is fine?
No, and this does not claim so. Some sites answer 200 with a "not found" message in the body. Checking that would mean reading and interpreting page content, which this deliberately does not do — notClaimed says so on every row.
Other tools by NeverEmpty
Every NeverEmpty Actor follows the same rule: it never returns an empty result to mean two different things, and it only charges for rows that actually carry an answer.
- seo-site-audit - technical SEO audit: meta, canonical, robots, headings
- domain-lookup - WHOIS, DNS, MX, SPF, DMARC and SSL expiry for a list of domains
- website-contacts - emails, phones and social profiles from any website, no invented addresses
Support
Found a link classified wrongly? Open an issue on the Issues tab with the URL and the row you got. Reports about incorrect data are fixed first.