Broken Link Checker - Find 404 & Dead Links on Any Website
Pricing
Pay per event
Broken Link Checker - Find 404 & Dead Links on Any Website
Crawl a website and find every broken link and image (404, 410, 5xx, dead domains, SSL errors) with the page and anchor text. Alerts on new broken links. $1/1k pages + $0.50/1k links.
Pricing
Pay per event
Rating
0.0
(0)
Developer
Yukai Lin
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
19 minutes ago
Last modified
Categories
Share
What does Broken Link Checker do?
Broken link checker for any website: it crawls your site, collects every link and image on every page and checks each one, so you get every broken (404) and dead link with the pages where it appears and its anchor text. Reasons include 404, 410, server errors, dead domains and expired SSL certificates, and a shareable Markdown report lists the fixes per page. Monitoring mode reports only links that broke since the last run.
How to use it (about 2 minutes)
- Enter your website URL in URLs or domains (
https://www.example.com/or justexample.com; one per line for several sites). A bad line never stops the run: it becomes a row witherrorType: "invalid_input"(not charged). - Set Max pages to crawl (every link and image found on those pages is checked).
- Optional: choose What to report (only problems, problems + internal redirects, or everything), turn on Check CSS, JavaScript and iframes or Also crawl the pages in the sitemap, turn off Check external links to check only your own site, or limit the crawl with include/exclude patterns.
- Click Start. Open the Report in the Output tab for a readable fix list, or export the dataset as CSV, Excel or JSON. Each broken link row says which page links to it (
firstFoundOn), the link text, the status and a one-sentencefix.
No broken links? The dataset has one "No broken links found" row. A link that answers 401/403/429 or sits behind bot protection (LinkedIn, Cloudflare challenge pages) is not reported as broken: it is counted as restricted in SUMMARY (turn on Report restricted links to list them).
- 🕷️ Crawls the whole site (or one folder) starting from the home page, and optionally every page in your XML sitemap
- 🔗 Links, images, CSS, JavaScript and iframes:
<a href>and<img>by default, stylesheets, scripts and iframes on request; each unique URL checked only once per run - 📍 Tells you where: source pages and link text for every broken link
- ❌ Clear reasons:
404,410,500,DNS: domain not found,TLS: certificate expired,connection refused - ↪️ Redirect report: internal links that go through a 301/302 (they should point at the final URL), with the full redirect chain
- 📋 Full link inventory on request: every checked link with status, response time, type (
link,image,stylesheet,script,iframe),nofollowand a slow flag - 🛡️ Few false alarms: links are checked exactly as written (
/page/and/pagecan differ), HEAD requests are confirmed with GET, cookie redirects are followed, timeouts are re-checked slowly for free, sites that block bots (LinkedIn, X/Twitter, Facebook…) and bot-protection challenges (Cloudflare, DataDome) answer 401/403/429/503 and are counted asrestricted, not broken, and pages that answer 200 but say "page not found" are caught assoft-404 - 🌐 JavaScript sites: an optional browser mode finds links that only appear after JavaScript runs
- 📈 Monitoring mode: on a schedule, report only the links that broke since the last run, plus the ones that were fixed
- 💸 Cheap: $1 per 1,000 pages crawled + $0.50 per 1,000 links checked
Who is it for?
- SEO and site owners: broken links waste crawl budget and hurt user experience. Find them before your visitors do
- Agencies: run a link audit for a client site in minutes and export it as CSV or Excel, or share the Markdown report
- Docs and blog maintainers: catch links to pages that moved or disappeared
- Monitoring: schedule a weekly run with Only report new broken links and get notified (Apify integrations: email, Slack, webhooks) only when new broken links appear
How much does it cost?
| Event | Price |
|---|---|
| Crawled page | $1 / 1,000 pages |
| Checked link (links, images and assets) | $0.50 / 1,000 unique links |
Example: a 500-page site with 3,000 unique links costs 500 × $0.001 + 3,000 × $0.0005 = $0.50 + $1.50 = $2.00.
No start fee. Pages that fail to load are not charged as crawled pages, and the free re-check of timeouts is not charged again. Use Max links to check as a cost cap. Higher Apify plans get volume discounts (see the Pricing tab).
Control your cost
- Two events: each crawled page, and each unique link, image or asset checked. Links are de-duplicated across the whole site (a menu link on every page is checked once), but there are usually many more links than pages, so the link checks are most of the cost. Real examples (September 2026): books.toscrape.com, 3 pages → 94 unique links → $0.003 for pages + $0.047 for links; python.org, 10 pages → 363 unique links → about $0.19 in total.
- Before the crawl, the run logs its page plan (Max pages × page price). After the crawl, before any link is checked, it logs how many unique links were found and what checking them will cost, and saves it as
estimatedLinkCostUsdinSUMMARY(both plans are incostPlan). - Cap it: Max links to check stops after that many unique links (internal links first, then images and assets, then external links). The Console form starts at 2,000 links (at most $1.00 on the free plan); API calls have no cap unless you set
maxLinksToCheck. - Free (
charged: false): a link that only redirects to a page already crawled (it is checked as a link but not read or charged as a second page), invalid input lines (errorType: "invalid_input"), pages that fail to load or time out, the slow re-check of timeouts,no-longer-linkedrows in monitoring mode and the summary row. - When your maximum charge per run is reached, the run stops: the status message says so,
SUMMARY.statusisLIMIT_REACHEDandSUMMARY.notProcessedhas the number of pages not crawled and links not checked, with the first 100 URLs. - If Apify restarts the run (server migration or Resurrect), pages and links charged before it are read or checked again for the report, but not charged again, and no row is written twice (
SUMMARY.resumed).
Cost comparison (checked September 2026)
Same job for every Actor: a real 10-page crawl of python.org with 363 unique links and images (September 2026).
| Actor | Published price (free plan) | Cost of this job |
|---|---|---|
| Broken Link Checker (this Actor) | $1 / 1,000 pages + $0.50 / 1,000 links | about $0.19 |
| khadinakbar/broken-link-checker | $1 / 1,000 links | about $0.36 |
| smart-digital/website-broken-links-redirects-checker | $20 / 1,000 pages + $2 / 1,000 links | about $0.93 |
| parseforge/broken-link-checker | $1.20 / 1,000 results + $0.04 start fee | — (checks a list of URLs; does not crawl a site) |
| jancurn/find-broken-links | Free; you pay Apify platform usage (its README estimates about $0.25 / 1,000 results) | — (last updated January 2025; no image or asset checks, no false-alarm handling) |
Input example
{"urls": ["https://www.python.org/"],"maxPages": 10,"maxLinksToCheck": 2000,"checkExternalLinks": true,"checkImages": true,"report": "problems+redirects"}
Use urls (a plain list of strings) in API calls. The older startUrls field ([{ "url": "https://…" }]) still works and can hold uploaded or linked URL list files, but Apify rejects the whole request with HTTP 400 if one of its entries is a bare domain or a blank line.
Output example
Real results from a 10-page crawl of python.org (September 2026). A partner link whose server answers 503:
{"url": "http://www.ironport.com/","site": "python.org","startUrl": "https://www.python.org/","status": 503,"statusClass": "server-error","statusText": "Service Unavailable","error": null,"finalUrl": "http://www.ironport.com/","redirects": 0,"internal": false,"linkType": "link","nofollow": false,"responseMs": 1771,"slow": false,"foundOnPages": 1,"firstFoundOn": "https://www.python.org/about/quotes/","firstAnchorText": "IronPort Systems","sources": [{ "page": "https://www.python.org/about/quotes/", "text": "IronPort Systems" }],"charged": true}
A link to a server that no longer accepts connections (unreachable links carry an errorType):
{"url": "http://pragprog.com/book/gwpy2/practical-programming","status": null,"statusClass": "error","error": "connection refused","errorType": "network","internal": false,"linkType": "link","foundOnPages": 1}
With Problems + internal links that redirect, internal links that go through a redirect are listed too, e.g. python.org's menu links https://www.python.org/psf/ (301 → /psf-landing/, on 10 pages) and https://www.python.org/doc/av (301 → /doc/av/), each with redirectStatus and redirectChain.
sources lists up to 20 pages per link; foundOnPages counts them. For spreadsheets, firstFoundOn and firstAnchorText give the first page and link text as plain columns (the table views use them instead of sources). site is the start site (host without www.) of the page where the link was first found, and startUrl the start URL that page was reached from. www.example.com and example.com count as the same site when deciding internal.
Monitoring: only new broken links
Turn on Only report new broken links (monitoring) and schedule the Actor (for example weekly). The first run is the baseline and reports every broken link. After that, the dataset only contains:
- links that are newly broken since the last run (
"change": "new"), and - links that were fixed (
"change": "fixed"), withfixedReason:now-works(the link answers now) orno-longer-linked(it is no longer on the site).
Broken links already reported last time are counted as stillBroken in SUMMARY but not output again, so an Apify notification only fires when something changed. Runs with the same Monitor name are compared (default: the start URL). If a run does not cover the whole site (page, link or spending limit), links it did not see are kept and not reported as fixed.
Real second run on badssl.com (September 2026), after one link was made to look new and two to look previously broken:
[{ "url": "https://expired.badssl.com/", "statusClass": "error", "error": "TLS: certificate expired", "change": "new", "foundOnPages": 1 },{ "url": "https://sha256.badssl.com/", "change": "fixed", "fixedReason": "now-works", "previousStatus": 404, "status": 200 },{ "url": "https://badssl.com/old-page-removed", "change": "fixed", "fixedReason": "no-longer-linked", "previousStatus": 404 }]
SUMMARY: "comparison": { "firstRun": false, "newBroken": 1, "stillBroken": 35, "fixed": 2, "partialRun": false }. The REPORT starts with a Since the last run section listing the new and fixed links. Pages and links are crawled, checked and charged the same way as without monitoring.
Summary and report
The key-value store contains:
SUMMARY(JSON):status(SUCCESS,PARTIAL_RESULTS,FAILED,NO_RESULTS, orLIMIT_REACHEDwhen the spending limit or Max links to check stopped the run), pages crawled, links checked per type, broken, server errors, unreachable,softNotFound, restricted, redirects (internal), slow links,linksFound/linksNotChecked,pagesFoundNotCrawled,estimatedLinkCostUsd,costPlan,invalidInputs,comparisonin monitoring mode and, when the run stopped at your limit,notProcessedREPORT(Markdown): the broken links most widespread first (fixing one menu link fixes it on every page), a fix list grouped by page, and the internal links that redirect
Every row has a statusClass: broken (404, 410 and other 4xx), soft-404 (HTTP 200 but the page says "not found"), server-error (5xx), error (DNS, TLS, timeout, connection or redirect-loop failure), restricted (401/403/429/999 or a bot-protection challenge: not necessarily broken; only listed with Report restricted links), redirect and ok (only with report "all"). Problem rows also have a one-sentence fix and, where it helps, a note.
From the same run: "pagesCrawled": 10, "linksChecked": 363, "byLinkType": { "link": 343, "image": 20 }, "serverErrors": 1, "networkErrors": 3, "internalRedirects": 16.
If nothing is reported, the dataset contains a single "No broken links found" row (or "No new broken links since the last run" in monitoring mode) with the summary. That row has "type": "summary" and "url": null, so an integration (n8n, Make, Zapier) can filter it out. If a start page cannot be read (blocked by a bot check, an error or a time-out), nothing on that site was checked: the dataset gets a "type": "start-page-error" row with the reason (not charged), SUMMARY.status is FAILED (or PARTIAL_RESULTS when other start pages worked), and the status message says so instead of "0 broken".
Use with AI agents (MCP)
Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/broken-link-checker) to Claude, Cursor or any MCP client, then ask e.g. "Check example.com for broken links and images and tell me which page to fix first."
{ "urls": ["https://example.com/"], "maxPages": 50, "maxLinksToCheck": 2000 }
Unreachable links carry an errorType (network, timeout…), and pages that fail to load are not charged.
Use it from code and integrations
Run it from your own code with the Apify API. This call waits for the run and returns the results as JSON (replace YOUR_TOKEN with your Apify API token):
curl -X POST "https://api.apify.com/v2/acts/tidytools~broken-link-checker/run-sync-get-dataset-items?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"urls":["https://www.python.org/"],"maxPages":10,"maxLinksToCheck":2000}'
The synchronous endpoint waits up to 5 minutes. For bigger runs, start the run with POST https://api.apify.com/v2/acts/tidytools~broken-link-checker/runs and read the dataset when it finishes, or use the apify-client package for JavaScript or Python.
Schedules and integrations: run it daily or weekly with Apify Schedules, get a webhook when a run finishes, or send the results to Zapier, Make, n8n, Google Sheets, Slack and other apps with Apify integrations. Results can be exported as JSON, CSV, Excel or XML.
Tips
- Start with a small Max pages to see the result, then raise it for the full site.
- Links to login pages or social networks often answer 401/403 to bots. They are counted as
restricted(seeSUMMARY.restricted), each with anote. Turn on Report restricted links to list them and open them in a browser to confirm. - If the status line says "Stopped at Max pages", more pages were found than crawled (
SUMMARY.pagesFoundNotCrawled): raise Max pages to crawl. - Use Page reading mode: Browser for sites whose menus and links are built with JavaScript.
- Already have a list of URLs? Bulk URL Status & Redirect Checker (by TidyTools) checks a list directly, with full redirect chains.
- For a full on-page SEO report (titles, meta descriptions, H1, canonical, alt text), use SEO Audit Crawler (by TidyTools).
Advanced settings
- Respect robots.txt: off by default, because this Actor is meant for sites you own or may audit. Turn it on and pages robots.txt disallows are not crawled or charged (
SUMMARY.pagesSkippedByRobotsTxt); links to them are still checked. Link checks and page reads identify as TidyTools, never as a browser. - Slow link threshold and User-Agent for link checks.
- Plain HTTP requests from: Auto (recommended) reads pages from our servers and retries from Apify's network when a site blocks data-center requests, before falling back to a browser.
- Time limit per page: default 120 seconds for reading a page, all retries and the browser fallback included. A slower page is skipped and not charged (links to it are still checked). 0 = no limit.
- HTTP 429 (Too Many Requests): a link that answers 429 is checked once more after the wait the server asks for in
Retry-After(at most 10 seconds; 2 seconds when it gives none). If it still answers 429, it is reported asrestrictedwith anote, not as broken. The retry is free. - Proxy: optional Apify Proxy for page requests sent from Apify's network (billed to your Apify account). Link checks always come from Apify's servers.
Limitations
- In the default mode the crawler reads each page's HTML. Links that only exist after JavaScript runs need Browser mode.
- Up to 1,000 links and 300 images are collected per page.
- Only
http(s)links are checked.mailto:,tel:,javascript:links are skipped, and#fragmentanchors are not verified (/page#missing-idis checked as/page). - Soft 404s (HTTP 200 with a "not found" page) are detected on pages the crawl reads (by their title); a soft-404 URL that is only linked as an image or asset is not.
- Requests come from Apify's servers. A few sites block data-center traffic; such links may show as
restrictedorerroreven if they work in your browser.
FAQ
Does the Actor collect usage data? The Actor sends one anonymous usage counter per run (input type, item count range, outcome) to improve the tool; no URLs, content or account data.
What does this broken link checker find? It crawls your website, checks every link and image on every page and reports broken links (404, 410, server errors, dead domains, expired SSL certificates) with the pages where each one appears and its anchor text. Turn on Check CSS, JavaScript and iframes to check those URLs too. You also get a shareable Markdown report with a fix list per page.
Does it work on JavaScript sites? Yes. In the default mode the crawler reads each page's HTML, so links that only exist after JavaScript runs need Page reading mode set to Browser. Browser mode always renders pages, so links added by JavaScript are found too (slower).
Can I be alerted only when new dead links appear? Yes. Turn on Only report new broken links (monitoring) and schedule the Actor: after the first run, the dataset only contains links that are newly broken since the last run, plus links that were fixed. Runs with the same Monitor name are compared, so an Apify notification only fires when something changed.
Are links that answer 403 or 429 reported as broken? No. 401, 403, 429, LinkedIn's 999 and bot-protection challenges (Cloudflare, DataDome) are restricted: not counted as broken, hidden unless you turn on Report restricted links, and shown in the status line and SUMMARY.restricted. A 429 is retried once after the wait the server asks for. Genuine 404, 410, 5xx, dead domains and bad certificates are reported.
Does it check #anchors, mailto: and tel: links? No. Only http(s) URLs are requested; mailto:, tel: and javascript: links are skipped, and a #fragment is not checked against the target page.
Does it find pages that return 200 but say "not found"? Yes, on pages it crawls: a 200 page whose title says "Page not found" / "404" is reported with statusClass: "soft-404".
Does it follow robots.txt? Only if you turn on Respect robots.txt (off by default, meant for your own sites). Then disallowed pages are not crawled or charged. Checks are sent as TidyTools, with no bot-detection tricks and no logins.
What happens if my input is wrong? The run never fails because of a typo: every problem becomes a dataset row with charged: false and an errorType, and the run exits normally. An empty list, a line that is not a URL or domain, ftp:// and javascript: lines, and private, local or reserved addresses (localhost, 127.0.0.1, 192.168.x.x, 10.x.x.x, 169.254.x.x) are invalid_input rows and are never requested. A start page that cannot be read gets a start-page-error row with the reason in plain words (not_found, http_error, unreachable for a dead domain, Cloudflare 530/526 or a bad SSL certificate, unsupported for a non-HTML URL, robots_disallowed when Respect robots.txt is on); such a page does not use up Max pages, so the other sites in the list are still checked. Text where a number is expected ("abc" for Max pages to crawl) falls back to the default.
What if a page links to localhost or a private address? Those links cannot work for your visitors, so they are listed as a problem (errorType: "invalid_input", with a fix) but are never requested from our servers, and they are not charged.
What is charged and what is free? You pay $1 per 1,000 crawled pages plus $0.50 per 1,000 unique links checked, with no start fee; links are de-duplicated across the whole site. Invalid input lines, links to private or local addresses, pages that fail to load or time out, redirect aliases of pages already crawled, and the slow re-check of timeouts are free. Use Max links to check as a cost cap.
Support
Open an issue in the Issues tab with the website URL. Issues are checked regularly.
Found this useful? A short review on the Store helps other users find it.