Bulk URL & Broken Link Checker avatar

Bulk URL & Broken Link Checker

Pricing

from $1.00 / 1,000 url checkeds

Go to Apify Store
Bulk URL & Broken Link Checker

Bulk URL & Broken Link Checker

Check URLs in bulk: status code, every redirect hop, final and canonical URL, response time, content type and size. Paste URLs, check a whole sitemap, or find broken links on your pages. HEAD with GET fallback, robots.txt respected, only-changed monitoring.

Pricing

from $1.00 / 1,000 url checkeds

Rating

0.0

(0)

Developer

Michael Costa

Michael Costa

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

11 hours ago

Last modified

Share

Bulk URL & Broken Link Checker checks URLs in bulk and finds broken links. Give it a list of URLs, a sitemap or pages to scan; for each URL you get the HTTP status code, every redirect hop, the final and canonical URL, and the response time.

Three ways to choose what to check
Check these URLsPaste a list: a migration's redirect map, a backlink list, a CSV column.
All URLs in a site's sitemapGive a website or a sitemap URL; every URL its sitemaps list is checked.
Links on these pagesGive pages; every link on them is checked, with the pages it was found on. Same-site pages can be followed a few clicks deep. That's a broken link check.

Try it in one click: the input comes pre-filled with four apify.com addresses: a page, an http:// address that redirects, a docs page and a page that doesn't exist. That's 4 results, about $0.004 (4 × $0.001, plus $0.00005 for the run start). Then replace them with your own URLs.

Monitor URLs: get only the ones that changed, in Slack, email or a webhook

With Only report URLs that changed since the last run on, each run reports just the URLs whose outcome, status code, final URL or canonical URL changed: the ones that broke, recovered or started redirecting somewhere else. A run where nothing changed returns 0 results and costs only the $0.00005 start fee.

  1. Fill in What to check and URLs (for example sitemap and your site), turn on Only report URLs that changed since the last run ("onlyChanged": true), and click Start. This first run reports every URL, and is the baseline the next runs compare with.
  2. Click Save as a new task (top right of the actor page). The memory belongs to that exact input (the same mode, URLs and options), so the task keeps comparing with its own last run. Changing the input starts a new baseline.
  3. In Apify Console, open Schedules, click Create new, set how often in Schedule setup (for example daily at 07:00), then Add your task.
  4. On the task, open the Integrations tab and pick where the changes go:
    • Slack: click Configure, sign in, pick the workspace and channel, and the "run succeeded" event. A useful message: {{resource.statusMessage}} (the example below's repeat run said "4 URLs checked (3 ok, 1 broken); 4 unchanged since the last run, not repeated") and a link to the results, <https://console.apify.com/storage/datasets/{{resource.defaultDatasetId}}|changed URLs>.
    • Gmail: click Connect with Google, set the subject and body, and attach the dataset (for example as CSV). It sends after each successful run.
    • HTTP webhook: event ACTOR.RUN.SUCCEEDED, your URL. Apify POSTs {"eventType": ..., "resource": {...}}; resource.defaultDatasetId is the run's dataset, and GET https://api.apify.com/v2/datasets/<defaultDatasetId>/items?format=json (with your API token) returns the changed URLs.

Apify's integrations fire after every successful run, including quiet ones: a quiet run's dataset is empty, and its status message says how many URLs were unchanged. Each result's previous field says what the URL's status code and outcome were last time.

FieldExampleNotes
urlhttp://apify.com/The URL checked.
outcomeokok, broken, refused, dns-error or error (see Output).
statusCode200The final answer's status code. null when no answer came back.
redirectCount1Hops before the final answer.
redirectChain[{"url": ..., "statusCode": 301, "location": ..., "timeMs": 41.2}, ...]Every response on the way, including the last.
finalUrlhttps://apify.com/Where the chain ended.
canonicalUrlhttps://apify.com/From the page's HTML or the Link header; null if none.
responseTimeMs160.1Summed over the chain.
contentType, sizeBytestext/html, 412345From the final answer.
foundOn, linkText["https://example.com/blog/"], "Our old post"Links mode only: where the link was and its text.
previous{"statusCode": 200, "outcome": "ok"}Only-changed mode: last run's answer; null when new.
errornullThe reason, for anything but ok.

One row per URL, checked once per run even when it's listed or linked many times. The full list is under Output.

You pay per URL in the results: $1.00 per 1,000 URLs checked, plus $0.00005 each time a run starts.

  • The example below: 4 URLs × $0.001 = $0.004, plus the start fee; the repeat run with nothing changed, $0.00005.
  • A month, for example: a daily check of a 1,000-URL sitemap with only-changed on: the first run is 1,000 × $0.001 = $1.00; if 20 URLs change on each of the other 29 days, that's 580 × $0.001 = $0.58; plus 30 starts ($0.0015): about $1.58.
  • Caps: Max results per run in the input, and Maximum cost per run in the run options. The run stops cleanly at whichever comes first; nothing is requested past the limit, so a capped run is also a fast one.

Never charged: URLs that were never requested: addresses robots.txt disallows, private or local addresses, ports other than 80/443, addresses that aren't valid URLs, sites that ask for more than 30 seconds between requests, and (links mode) the other links to a site that answered a check with HTTP 429. They aren't in the results; they're listed in the NOT_CHECKED record of the run's key-value store (the "URLs not requested" link on the run's Output tab), counted in RUN_STATS, and named in the run log. They don't use up your maximum cost per run either. Sitemap files and robots.txt files read along the way are free, and so are URLs whose result didn't change (only-changed mode).

Charged, because they're answers: a URL that was requested and then redirected into one of the above (its redirect chain is the answer), and a dns-error (the domain was looked up and doesn't exist).

  1. Open Bulk URL & Broken Link Checker and click Try for free (or Start if you're signed in).
  2. Pick What to check: urls, sitemap or links.
  3. Paste your addresses into URLs, one per line (example.com means https://example.com/).
  4. Optional: for links mode, set Crawl depth and Max pages to read; turn on Only report URLs that changed since the last run for monitoring.
  5. Click Start, then open the Output tab and export as JSON, CSV or Excel.

Example: four addresses, then a repeat run

The pre-filled input, with Only report URLs that changed since the last run turned on:

{"mode": "urls", "urls": ["https://apify.com", "http://apify.com", "https://docs.apify.com/platform",
"https://docs.apify.com/this-page-does-not-exist"], "method": "auto", "readCanonical": true, "onlyChanged": true}

Two of the four results (real output from a local run on 2026-09-25, some fields left out):

[
{"url": "https://docs.apify.com/platform", "outcome": "ok", "statusCode": 200, "redirectCount": 1,
"redirectChain": [
{"url": "https://docs.apify.com/platform", "statusCode": 301, "location": "https://docs.apify.com/", "timeMs": 25.9},
{"url": "https://docs.apify.com/", "statusCode": 200, "location": null, "timeMs": 73.7}],
"finalUrl": "https://docs.apify.com/", "canonicalUrl": "https://docs.apify.com/", "responseTimeMs": 99.6,
"method": "GET", "previous": null},
{"url": "https://docs.apify.com/this-page-does-not-exist", "outcome": "broken", "statusCode": 404,
"redirectCount": 0, "finalUrl": "https://docs.apify.com/this-page-does-not-exist", "method": "GET",
"previous": null}
]

Run again straight after, the same input returned 0 results: nothing had changed, so nothing was charged but the start fee.

Input

FieldWhat it does
What to check (mode)urls: each line is a URL. sitemap: each line is a website or sitemap URL. links: each line is a page whose links are checked.
URLs (urls)One per line. example.com means https://example.com/.
Request method (method)auto (HEAD, GET when needed; recommended), head or get.
Read the canonical URL (readCanonical)On by default. Off: HTML pages are checked with HEAD too (faster, lighter); a canonical URL in the HTTP Link header is still reported.
Crawl depth (maxDepth, links mode)0 (default): the links on your pages. 1: also read the same-site pages they link to, and check their links. Up to 5.
Max pages to read (maxPages, links mode)Pages read for links, your start pages included. Default 20.
Check links to other sites (checkExternalLinks, links mode)On by default. Other sites' pages are checked, never crawled.
Also check images, scripts and stylesheets (includeResources, links mode)Off by default.
Max URLs per site (maxUrlsPerSite, sitemap mode)Default 500, up to 50,000.
Only report URLs that changed since the last run (onlyChanged)Off by default.
Max results per run (maxResults)Cap the total number of URLs in the results.
{
"mode": "links",
"urls": ["https://example.com/blog/"],
"maxDepth": 1,
"maxPages": 50,
"maxResults": 2000
}

Output

One row per URL, checked once per run even when it's listed or linked many times. Fields that don't apply are null.

{
"url": "http://apify.com/",
"outcome": "ok",
"statusCode": 200,
"ok": true,
"redirectCount": 1,
"redirectChain": [
{"url": "http://apify.com/", "statusCode": 301, "location": "https://apify.com/", "timeMs": 41.2},
{"url": "https://apify.com/", "statusCode": 200, "location": null, "timeMs": 118.9}
],
"finalUrl": "https://apify.com/",
"canonicalUrl": "https://apify.com/",
"responseTimeMs": 160.1,
"contentType": "text/html",
"sizeBytes": 412345,
"method": "GET",
"error": null,
"source": "http://apify.com",
"foundOn": null,
"foundOnCount": null,
"linkText": null,
"previous": null,
"checkedAt": "2026-01-01T00:00:00Z"
}
  • outcome: ok (final status 2xx), broken (other 4xx and 5xx, and 3xx without a target), refused (401, 403, 407, 429, 451), dns-error (the domain doesn't resolve), error (timeout, TLS failure, more than 10 redirects, and the like; error says which). After a redirect, also robots-blocked, not-public (a private network address, a port other than 80/443, or not http/https) or invalid-url for where it pointed.
  • redirectChain: every response on the way, including the last. When a redirect leads somewhere that can't be requested (robots.txt, a private address), the chain shows the hops up to there and finalUrl is where it stopped.
  • responseTimeMs: from sending each request to receiving its response headers, summed over the chain.
  • sizeBytes: the Content-Length header, or the downloaded size when the whole body was read.
  • source: the input line the URL came from. foundOn, foundOnCount, linkText (links mode): the pages read this run that link to it (first 20), how many there are, and the link's text or image alt text.
  • previous (only-changed mode): the status code and outcome from the last run that reported this URL; null when it's new.

URLs not requested (NOT_CHECKED)

Every run writes a NOT_CHECKED record to its default key-value store:

{"count": 3, "items": [...], "truncated": false}
. Each item has url, outcome (robots-blocked, not-public, invalid-url, error for a site asking for more than 30 s between requests, or rate-limited in links mode), error (the reason), source, foundOn, foundOnCount and checkedAt. The first 10,000 are listed; count is the full number. RUN_STATS has notChecked and notCheckedOutcomes.

Run it on a schedule, or from your own code

  1. Save your input as a task and add it to a schedule (Console → Schedules), as in Monitor URLs above.
  2. Collect results: download the dataset as JSON, CSV or Excel; fetch the latest run's results from the API (GET https://api.apify.com/v2/actor-tasks/<task id>/runs/last/dataset/items?status=SUCCEEDED&format=csv, with your API token); let a webhook tell your system when a run succeeds; or connect it to Make, Zapier or n8n through Apify's integrations.

The only-changed memory lives in a key-value store in your own account (url-checker-memory), per input, so a task keeps its memory from run to run.

Yes, through Apify's MCP server: add https://mcp.apify.com?tools=humble-echidna/url-checker to your MCP client (or let the agent find it with the server's actor search). The agent passes the same input as above, e.g. {"mode": "urls", "urls": ["https://example.com/old-page"]}; keep maxResults small for quick answers.

Who it's for

People who own a list of URLs and need to know when one breaks or starts redirecting somewhere else: SEO and web teams after a migration (checking the redirect map), site owners watching their sitemap, and anyone checking a backlink or partner-link list.

Why this one?

  • The whole redirect chain, hop by hop: each hop's address, status code, where it pointed and how long it took. Not just the first and last status.
  • HEAD first, GET only when needed. Files are checked with a HEAD request, so nothing is downloaded. Many servers answer HEAD wrongly (or not at all), so an error answer, or a HEAD request that fails, is confirmed with GET before a URL is reported broken. Pages are read with GET when you want their canonical URL (one request, not two).
  • Clear outcomes, not just numbers: ok, broken, refused (the site turned away an automated check; it may work in a browser), dns-error or error, with the reason. URLs it won't request (robots.txt, private or invalid addresses) are listed separately, free.
  • Only changed since last run: schedule it, and each run reports just the URLs whose result changed. Good for monitoring redirects and catching new broken links.
  • Polite and safe. It identifies itself honestly (User-Agent HumbleEchidnaApify), follows each site's robots.txt (read once per site per run) and Crawl-delay, sends at most 2 requests at a time to one site, and only requests public web addresses on the standard ports (80 and 443).
  • Reliable. One failing URL, page or sitemap never affects the others in your run. The run log and the RUN_STATS record say exactly what happened.

Limits

  • Only public web addresses on the standard ports (80 for http, 443 for https) are requested, for every redirect hop too. Any other address is never requested (see NOT_CHECKED; free); a redirect to one stops there.
  • robots.txt is respected on every hop: a disallowed URL is never requested (see NOT_CHECKED; free). A site whose robots.txt can't be read (server error) is treated as disallowing everything for the run, as the robots.txt standard says. A site that asks for more than 30 seconds between requests isn't requested either (NOT_CHECKED, outcome error, free).
  • No proxies and no login: pages behind a login, a paywall or bot protection come back as refused (usually 403).
  • A failed request is retried once. Each URL gets up to 60 seconds in total. Pages are read up to 2 MB.
  • Links mode reads HTML as the server sends it: links added by JavaScript after the page loads aren't seen.
  • Links mode checks the links to different sites in turns, at most 2 at a time per site, so a slow site doesn't hold up the rest. A site that answers a link check with HTTP 429 (too many requests, even after one retry) is left alone for the rest of the run: that link is a refused result, and the site's other links aren't requested (NOT_CHECKED, outcome rate-limited, free; the site is named in linkHostsThrottled in RUN_STATS). Large sites such as GitHub often do this when a page links to hundreds of their pages.

FAQ

Why is a URL refused when it opens fine in my browser?

Some sites block automated requests or require a login. This checker doesn't get around that, so it reports what the site answered.

Yes: turn on Only report URLs that changed since the last run. A URL counts as changed when its outcome, status code, final URL or canonical URL differs from the last run of the same check (same mode, URLs and options). Response time and size don't count. The memory lives in a key-value store named url-checker-memory in your own account. See Monitor URLs above.

Why did a URL or input return nothing?

Look in NOT_CHECKED (URLs that were never requested, with the reason) and in the run log and RUN_STATS, which name the input line and what went wrong; every other URL in the run is unaffected.

It requests only the addresses you give it, or that your pages and sitemaps list, the way a browser or a search engine would: no login, honouring robots.txt, identified by its User-Agent. It collects HTTP metadata about URLs, not personal data. You're responsible for checking only sites you're allowed to.

ActorUse it when
Sitemap URL ExtractorYou want the sitemap's URLs themselves (with lastmod, images, hreflang) rather than their status.
SEO Audit CrawlerYou want a full on-page audit (titles, H1s, canonicals, broken links per page) of the same site, for weekly or monthly reports.

Feedback and support

Found a bug, or need a field or option that isn't here? Open an issue on the Issues tab with the input you used.

Versions

Current version: 1.0. See the Changelog tab for what changed in each version.