Broken Link Checker: Find Dead Links Site-Wide
Pricing
from $0.43 / 1,000 link checkeds
Broken Link Checker: Find Dead Links Site-Wide
Find broken links and 404 errors on a page, a list of pages, a whole site or its sitemap. Every link is followed to its final HTTP status: one row per link with the status code, verdict, anchor text, source page and redirect chain. No login.
Pricing
from $0.43 / 1,000 link checkeds
Rating
0.0
(0)
Developer
Pradio Actors
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
7 hours ago
Last modified
Categories
Share
What does Broken Link Checker do?
Broken Link Checker finds the broken and dead links on a page, a list of pages, a whole site or its sitemap, and returns one row per link checked. Each row carries the checked URL, its final HTTP status, an ok, broken or error verdict, the anchor text, the page it appeared on and every redirect it took. Paste URLs or bare domains, pick a mode, press Start. Nineteen fields land on each data row. It reports broken links; it does not fix anything.
Each checked link costs $0.0008, billed only after its row is in your dataset. In a measurement on 40 sites it had never seen, 35 answered with checked links, a hit rate of 87.5% (3 returned no checkable links, 2 errored). A run that finds no links returns one uncharged row that says so.
Who uses Broken Link Checker
| Buyer | What they run it for |
|---|---|
| Website owners | Finding dead links on their own pages before visitors or crawlers do. |
| SEO practitioners | Finding dead links that leak ranking; each row names the page and the anchor to fix. |
| Teams after a migration or redesign | Checking that old URLs redirect somewhere instead of dying. |
| Content maintainers | Auditing outbound links that rot quietly over time. |
Features
- A page, a list, a whole site or its sitemap.
modepicks what is read: only the pages you list, a crawl of each site from the page you give, or every page the site's sitemap lists. - Crawls politely. Crawl and Sitemap modes stay on your site, read at most
maxPagespages (never more than 2,000), honour robots.txt and its crawl delay, never read a page robots.txt disallows, and stop reading a site that refuses a page. - Only the broken ones, if you want. Turn on
onlyBrokenand the dataset holds only broken and unanswered links, and you pay only for those. - HEAD first, GET when refused. A cheap HEAD request leads; a server that answers 405 or 501 is asked again with GET.
- The whole redirect chain. Up to 10 redirects are followed per link;
redirect_chainlists each one with its status, andfinal_urlandredirect_countare read, not assumed. - Three honest verdicts.
okbelow HTTP 400,brokenat 400 or above,errorwhen a link never answers within 15 seconds, with the reason inerror_message, which stays empty on every link that answered. - Links, and assets when you ask.
checkAssetsadds the images, scripts and stylesheets each page loads;checkExternaloff keeps the check to your own site. - Checked once across pages. A footer link repeated across pages is checked once;
all_sourceslists every place it appeared. - Unusable entries answered, not dropped. An entry in
queriesthat is not a fetchable URL gets its own free row withstatusbad_urland the reason. - Plain HTTP, no browser, no login. Up to 8 links are checked at once, with a 15 second limit per request. A server that asks the checker to slow down gets one wait of the time it asks for.
- Caps you control.
maxResultsPerQuerylimits the links checked per page;maxItemslimits the whole run, 100 by default.
What you can count on
- You pay only for rows the run judged; a row it could not judge is pushed as an uncharged
ITEM_STATUSrow with the reason on it. - Every row is charged only after it is written to your dataset; a row you cannot see is never billed.
- A run that finds nothing returns one
NO_RESULTSrow that says so, never an empty dataset, and it is not charged. - A spending limit stops the run cleanly: one
STOPPED_EARLYrow reports how many rows were returned and how many were not. - Every run writes a
RUN_SUMMARYentry withrowsFetched,rowsPushed,rowsChargedandduplicatesDropped, so a short run and a broken one are told apart. - No value is invented: a field the checked page does not show is null, and the output table says which.
Why this one
- This Actor bills $0.0008 per checked link; the most-used alternative on this platform bills $0.001, measured on 2026-09-16.
- Started on the same site that day, the alternative returned 19 rows and this Actor returned 11. The counts are not the same job twice: the alternative's run crawled on from the start page it was given, while this Actor read only the pages it was given on that site.
- The hit rate above is measured on sites this Actor had never seen, not on a best case.
- Every row names the page the link was found on, so a dead link is a fix, not a riddle.
What data does Broken Link Checker return?
One real row from a run over the example pages in the default input:
{"url": "https://crawler-test.com/links/not_found/foo1","final_url": "https://crawler-test.com/links/not_found/foo1","status": 404,"status_text": "Not Found","classification": "broken","is_broken": true,"source_domain": "crawler-test.com","source_url": "https://crawler-test.com/links/broken_links_internal","anchor_text": "Broken Internal Link 1","element": "a","all_sources": [{"source_url": "https://crawler-test.com/links/broken_links_internal","anchor_text": "Broken Internal Link 1","element": "a"}],"method": "HEAD","redirect_count": 0,"redirect_chain": [],"content_type": "text/html","error_message": null,"duration_ms": 277,"checked_at": "2026-09-25T09:15:53.381Z","row_type": "ROW"}
Each row is one link checked end to end. Every field below sits on every data row; a value a check cannot produce is null, never absent.
| Field | What it is |
|---|---|
url | The link as it was found, made absolute. This is the row's key: one row per unique link URL. |
final_url | Where the link ended after its redirects; the same as url when it never redirected, null when it never answered. |
status | Two shapes by design, documented in the table below: an integer HTTP status on a link that answered, or a miss word such as error or bad_url on an ITEM_STATUS row. |
status_text | The server's status line, like Not Found, or the failure reason when nothing answered. |
classification | The verdict on the row: ok, broken or error. |
is_broken | true on a confirmed 4xx or 5xx, false on a healthy answer, null when unproven. |
source_url | The page this link was first found on; for a sitemap-listed page that failed, the sitemap that listed it. |
anchor_text | The clickable text of the link on that page; an image's alt text when assets are checked. |
element | The HTML element the link came from: a for a link, img, script or link for an asset, sitemap for a page a sitemap listed. |
all_sources | Every page and anchor text the link appeared on: one entry when a single page carries it, one per page when several do. Empty on a row for an entry that could not be read. |
method | HEAD or GET: how the check was made. |
redirect_count | How many redirects were followed before the final answer; a URL answered directly shows none. |
redirect_chain | Every redirect in order: the address that answered, its status code and where it pointed. Empty when the link answered directly. |
content_type | The media type the final address answered with, like text/html or image/png; null when the server sent none or never answered. |
error_message | Why the link could not be checked, on an error row: a timeout, a refused connection, a name that does not resolve. Null on every answered link. |
duration_ms | How long the check took in milliseconds, redirects included. |
checked_at | The ISO timestamp of the check. |
source_domain | The host of the start page the link was found on (one of the pages you named); the link's own host is in url. |
row_type | ROW on every link that answered, ITEM_STATUS on a link that never answered; the status kinds below mark the run's own messages. |
The Overview tab shows the per-link fields; the All fields tab adds redirect_chain, content_type, error_message, duration_ms, checked_at and source_domain. error_message is filled only on error rows, so on a sweep where every link answered it is empty on every row.
status carries two shapes by design:
status value | On which rows | What it means |
|---|---|---|
An integer HTTP code, like 200 or 404 | ROW | The status the link's final address answered with. |
error | ITEM_STATUS | The link, or a start page you named, never answered: a timeout, a refused connection, a name that does not resolve. classification is error, the reason sits in error_message, and the row is free. |
bad_url | ITEM_STATUS | The queries entry was not a usable URL. The entry is named in url, the reason sits in reason, and the row is pushed free: a malformed entry is answered, never dropped. |
not_found | ITEM_STATUS | Sitemap mode found no sitemap listing a page for this site. The entry is in url, what was tried is in reason, and the row is free. |
blocked | ITEM_STATUS | Crawl mode: the site's robots.txt does not allow reading the start page, so it was not read. Free. |
row_type says what a row is. ROW is a link that answered. ITEM_STATUS is a link that never answered or an entry that could not be read: pushed so you see it, never billed. NO_RESULTS means no page produced a checkable link. STOPPED_EARLY means your spending limit ended the run. Status rows also carry the run's bookkeeping:
| Field | What it is |
|---|---|
reason | On a bad_url, not_found or blocked row, why that entry was not read. On NO_RESULTS or STOPPED_EARLY, why the run returned no link rows or stopped early. An error row carries its reason in error_message instead. |
rowsFetched | How many links were checked, counted before duplicates were dropped and any cap applied; on status rows only. |
rowsReturned | On a status row: the link rows in the dataset. On STOPPED_EARLY, the rows returned before the spending limit stopped the run; 0 on an empty-result row. |
rowsRemaining | On a status row: the rows not returned when the spending limit stopped the run (STOPPED_EARLY), and 0 on an empty-result row. |
HTTP status code cheat sheet
What the status on a checked link means, and what to do about it. Redirects (301, 302, 303, 307, 308) are followed. status is the code the final address answered with. Each redirect's own code sits in redirect_chain:
| Status | Verdict | What it means | What to do |
|---|---|---|---|
200 | ok | The page answered normally. | Nothing. |
204, 206 | ok | Answered with no content, or part of a file. | Usually nothing. |
301, 308 in redirect_chain | set by the final status | Moved permanently. final_url is where it ended. | Update the link to final_url so visitors skip the hop. |
302, 303, 307 in redirect_chain | set by the final status | Moved temporarily. | Fine for a login or a tracking link; check a long chain. |
A 3xx in status | ok | Not followed to its end. The 10-redirect limit was reached, the redirect named no new address, or the checker does not follow that code, such as 300 or 304. redirect_chain shows how far it got. | Open final_url by hand. A chain that long is worth fixing. |
400 | broken | The server rejected the request as malformed. | Check the URL for typos or bad characters. |
401, 403 | broken | The page needs a login, or refuses this request. | Fine if the page is private; otherwise the link points somewhere readers cannot go. |
404 | broken | Not found. | Fix the link, or redirect the old address. |
410 | broken | Gone on purpose. | Remove the link. |
429 | broken | Too many requests. The checker waited once as asked; the site still refused. | Re-run later, or with maxPages lower. |
500, 502, 503, 504 | broken | The server failed or was down when checked. | Re-run later; a link that stays 5xx is broken. |
error (no HTTP code) | error | No answer: a timeout, a refused connection, a name that does not resolve. The row is free. | Read error_message; re-run to rule out a passing outage. |
Pricing
You pay $0.0008 per checked-link row, the result-returned event, charged only after the row is written. Apify also bills its own apify-actor-start event once per run, $0.00005 at this Actor's memory size. Status rows, miss rows (error, bad_url, not_found, blocked) and dropped duplicate links are free. Every link that answered is billed, ok and broken alike: the status code is the answer you paid for. A link that never answered is an error row and is free, because nothing could be checked. With onlyBroken on, healthy links are checked but left out of the dataset, and you pay only for the broken rows you get; unanswered links still come back, free.
| Rows returned | Link charges | Start event | Total |
|---|---|---|---|
| 100 | $0.08 | $0.00005 | about $0.08 |
| 1,000 | $0.80 | $0.00005 | about $0.80 |
| 10,000 | $8.00 | $0.00005 | about $8.00 |
A start page carries more than one link. The 35 pages that answered in the measurement produced 3,218 link rows in total, about 92 per page, so budget by sweep:
- 100 start pages: about 88 answer, about 8,100 links checked, about $6.48 plus the start event.
- 1,000 start pages: about 875 answer, about 80,500 links checked, about $64.40 plus the start event.
Your pages will differ, and maxItems caps a run at 100 rows unless you raise it.
Duplicates are billed once, not once per sighting. If 50 link sightings across your pages deduplicate to 47 unique URLs, you pay for 47 checks; the repeats join all_sources free.
How do I use Broken Link Checker?
- Open the Actor and press Try for free.
- Paste your pages or sites into Queries, one URL or bare domain per line. The input carries three example pages from a public test site; replace them with yours.
- Pick a Mode: Pages for just those pages, Crawl for the whole site from each page, Sitemap for every page the sitemap lists.
- Optionally turn on Only broken links, and set Max pages and Maximum items.
- Press Start. Rows land in the dataset as links are checked, and the run ends when every page is read.
Example input:
{"queries": ["https://crawler-test.com/links/broken_links_internal", "https://crawler-test.com/links/broken_links_external", "https://example.com/"],"maxItems": 100}
Or start a run over the API:
curl -X POST "https://api.apify.com/v2/acts/Pradio~broken-link/runs?token=YOUR_APIFY_TOKEN" -H "Content-Type: application/json" -d "{\"queries\":[\"https://example.com/\"]}"
Input
| Input | Default | What it does |
|---|---|---|
queries | three example pages | The pages or sites to check, one URL or bare domain per line. An entry that is not a fetchable URL gets its own free bad_url row with the reason, so a typo is answered, never dropped. |
mode | pages | pages reads only the pages you list. crawl starts at each and follows links on the same site. sitemap reads the pages the site's sitemap lists. |
maxPages | 50 | In crawl and sitemap modes, the most pages one run reads for links, across all entries. pages mode reads exactly the pages you list, so this limit applies only to crawl and sitemap. |
onlyBroken | false | Return only broken and unanswered links. A broken link is charged; one that never answered is free. Healthy links are still checked, then left out. |
checkExternal | true | Check links to other sites too. Off keeps the check to the site you named. |
checkAssets | false | Also check the images, scripts and stylesheets each page loads. |
maxResultsPerQuery | none | The most new links checked from one page read. Unset means every link found is checked. |
maxItems | 100 | The most link rows one run returns in total. Rows past the cap are dropped, and the run summary shows how many links were checked. With onlyBroken on it counts broken rows. |
queries
Each entry is a full URL like https://example.com/blog, or a bare domain like example.com, which is fetched over HTTPS. In pages mode the page is fetched once, its anchors are collected and each unique link is checked. An entry nothing can be fetched for, a typo or an unrecognisable line, is answered on its own uncharged row: status bad_url, the entry in url, the reason in reason.
mode
pages(the default) reads exactly the pages you list and nothing else.crawlreads each entry, then the pages on the same site it links to, then theirs, breadth first, untilmaxPagespages are read ormaxItemsrows are found. It never leaves the site, reads the site's robots.txt first, skips any page robots.txt disallows and waits the crawl delay it asks for. Links to disallowed pages are still checked with one request each; their own links are not read.sitemapreads the site's sitemap: the entry itself when it is a.xmlsitemap URL, else the sitemaps robots.txt names, else/sitemap.xml. A sitemap index is followed into its child sitemaps on the same site. Up tomaxPageslisted pages are read for links, and a listed page that fails is itself a row, with the sitemap as itssource_url. A site with no readable sitemap gets one freenot_foundrow.
Raise maxItems for a crawl: at the default of 100 rows a crawl stops at the first hundred links.
{"queries": ["https://example.com/"],"mode": "crawl","maxPages": 50,"maxItems": 2000}
Worked examples
Use it to check a few pages you just published.
{"queries": ["https://example.com/blog/new-post", "https://example.com/pricing"],"mode": "pages"}
Use it to sweep a whole site and keep only the broken links.
{"queries": ["example.com"],"mode": "crawl","maxPages": 200,"maxItems": 1000,"onlyBroken": true}
Use it to check every page in a sitemap, the site's own links only.
{"queries": ["https://example.com/sitemap.xml"],"mode": "sitemap","maxPages": 500,"maxItems": 5000,"checkExternal": false,"onlyBroken": true}
Use it to find missing images, scripts and stylesheets on a landing page.
{"queries": ["https://example.com/"],"checkAssets": true,"onlyBroken": true}
Output
Rows land in the default dataset as links are checked. Four extras tell you how a run went:
ITEM_STATUS: a link or start page that never answered, withstatuserrorand the reason inerror_message; or aqueriesentry that could not be read, named inurlwithstatusbad_url,not_foundorblockedand the reason inreason. Pushed so you see it, never billed.NO_RESULTS: no start page produced a checkable link. One uncharged row carriesreasonandrowsFetched, so an empty answer is an answer, not silence.STOPPED_EARLY: your spending limit ended the run. One uncharged row carriesrowsReturnedandrowsRemaining; raise the limit and re-run for the rest.RUN_SUMMARY: an entry in the run's key-value store carrying the counts: links fetched, rows pushed, rows charged, duplicates dropped, stopped early or not.
A start page that cannot be reached is itself a free error row with the reason in error_message, so a run over pages that are all down still tells you so, page by page, and costs only the start event.
What can you do with the data?
Sweep your own site before a launch. Run the checker over the pages, filter classification to broken, and hand the list to whoever fixes it. Each row already carries the dead URL, the page it sits on and the anchor text to search for.
Audit a migration. Old URLs that still redirect show every hop in redirect_chain and where they landed in final_url. The ones that answer 404 instead are the ones to remap.
Watch outbound links on a schedule. Reference, partner and affiliate links rot quietly. Put the run on an Apify schedule, export the broken rows to a sheet, and the dataset becomes a monthly fix list.
Qualify a site you are evaluating. A page full of dead links says something about how it is maintained. One run counts them without a manual click-through.
Use Broken Link Checker with AI agents
Paste this line to give an agent this Actor through Apify's MCP server:
claude mcp add --transport http apify "https://mcp.apify.com?tools=Pradio/broken-link"
Personal data
- A row carries only the declared link-health fields: the checked URL, its HTTP status and verdict, the anchor text and the page it appeared on. No field names or identifies a person.
- A row keeps facts and the link's own anchor text, never the body or a substantial part of a page.
- Every row links back to the page it was found on, so what was collected is easy to check.
- A page that refuses the read is reported as a
brokenorerrorrow; nothing retries past a block or works around a refusal. - Personal data is not what a row is about, but it can appear incidentally inside anchor text or a URL path (a name in a link label, a profile slug in a path). Run it against sites you operate or are permitted to check.
- You choose the pages and sites, so you are the controller for the list you supply. In
pagesmode the Actor reads only what you list; incrawlandsitemapmodes it reads further pages of the same site, never another site, and never a page the site's robots.txt disallows. To exclude a section, disallow it in robots.txt or usepagesmode.
Release notes
0.1.22(2026-09-25): anerrorrow, a link or start page that never answered, is now free;statuscarrieserroron it androw_typeisITEM_STATUS.0.1.20(2026-09-24):modeaddscrawlandsitemapbesidepages; newonlyBroken,checkExternal,checkAssetsandmaxPagesinputs; newredirect_chain,content_typeanderror_messagecolumns. Existing inputs work as before.0.1.13(2026-09-18): an unusablequeriesentry now gets its own freebad_urlrow instead of being dropped; the redirect field isredirect_count.0.1.12(2026-09-17): the Store icon redrawn.0.1.11(2026-09-17): the sample row on this page re-synced to a fresh capture.0.1.10(2026-09-17): a link first seen without anchor text picks it up when a later page shows it.0.1.9(2026-09-17):source_urlandanchor_textnow always describe the same sighting.0.1.8(2026-09-17):RUN_SUMMARYis now an entry in the run's key-value store.0.1.7(2026-09-17):all_sourcesentries carry the element name.0.1.6(2026-09-17):all_sourcesentries use snake_case keys.
Limits
- It checks links and reports them; it does not fix links: repair stays with you.
- A crawl stays on the start page's site and reads at most
maxPagespages; a site larger than that is read in part. Pages robots.txt disallows are not read, so links that only appear on them are not found. - Anchor tags are checked by default; images, scripts and stylesheets only with
checkAssets.javascript:,mailto:,tel:,data:,sms:andftp:links are skipped. - Links a page builds with JavaScript after load are not seen: pages are fetched over plain HTTP, with no browser.
- Requests are logged-out and public, with a fixed declared user agent. There is no proxy rotation and no retry past a block: a site that refuses is a
brokenorerrorrow, not a workaround. A 429 gets one wait of the time the site asks for, and then the answer stands. - A start page that cannot be read is its own row:
errorwith the reason inerror_messagewhen nothing answered,brokenwith the status code when it answered 4xx or 5xx. It never disappears from the results. - A link that never answers within 15 seconds is an
errorrow, suspected but not proven broken. - The verdict is the HTTP answer. A page that answers
200with a "not found" message of its own (a soft 404) readsok, whatever its link text says. - A redirect chain is followed for at most 10 redirects; a longer chain returns the last redirect's status.
- Every link that answered is a billed row,
okorbroken. Anerrorrow, a link that never answered, is free, so a run of nothing but unanswered links costs only the start event.
Troubleshooting
The run returned fewer rows than maxItems.
maxItems is a ceiling, not a target. Fewer rows means the pages ran out of links first; rowsFetched in RUN_SUMMARY shows how many links were checked.
The dataset has one row saying NO_RESULTS.
No start page produced a checkable link. That is the answer to this run, not a bug, and the row is free.
A row says bad_url in status.
One queries entry was not a URL the Actor could fetch, a typo or an unrecognisable line. The row names the entry in url, gives the reason in reason, and is free. The other entries ran normally.
A row says error with is_broken empty.
The link never answered within 15 seconds. It is suspected, not proven dead, and the row is free; re-run later to retry it.
Every row is an error row for a start page I listed.
None of the pages you listed answered at the network level. Each row names its page in url and the reason in error_message: a timeout, a refused connection, a name that does not resolve. Check the URLs you pasted.
FAQ
Can I use integrations with Broken Link Checker? Yes. The dataset plugs into Apify integrations like Zapier, Make and webhooks, and Apify schedules can re-run the sweep weekly or monthly without you touching it.
Can I use Broken Link Checker with the Apify API? Yes. Start runs and read the dataset back over the API; the curl example above is the whole call.
Can I use Broken Link Checker through an MCP server? Yes. The line under "Use Broken Link Checker with AI agents" adds it to an agent through Apify's MCP server.
Is it legal to check links this way? The Actor makes the same plain HTTP requests any monitoring tool makes: it bypasses no login and keeps only link facts. You choose the pages, so point it at sites you are responsible for or allowed to test.
Something look wrong? Report it on this Actor's Issues tab in the Apify Console and it will be looked at.
Not affiliated
Broken Link Checker is an independent tool, not affiliated with or endorsed by any site you point it at. The example pages in the default input belong to crawler-test.com, a public test site.