# Broken Link Checker (`deriverge/broken-link-checker`) Actor

Crawls a website from a start page or sitemap and checks every link and image once. Each broken link comes with the page it is on, its anchor text, link type and status, and scheduled runs flag links that broke or were fixed since the last run.

- **URL**: https://apify.com/deriverge/broken-link-checker.md
- **Developed by:** [deriverge s.r.o.](https://apify.com/deriverge) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.40 / 1,000 links

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Broken Link Checker

Broken Link Checker crawls the pages of a website from a start page or from its sitemap, collects every link and image on them and checks each unique target once. Each row is one broken link on one page: the link, the page it was found on, its anchor text, link type, status code or error class. Enter your site in **Start URLs** and click **Start**; the prefilled practice site has broken images on purpose. Links to sites behind bot protection are reported as `restricted`, since their status cannot be verified. It costs $0.80 per 1,000 links on the Free plan ($0.40 on Business), and the $5 of monthly credit in Apify's Free plan covers about 6,200 links.

### What data does it return?

| Field | Description |
|---|---|
| `url` | The link target as an absolute URL, such as `https://example.com/old-page`. |
| `foundOn` | The crawled page that contains the link. For pages read from a sitemap it is the sitemap, and for the start URL itself it is empty. |
| `anchorText` | Visible text of the link. For a linked image or an `<img>` it is the alt text, and for an hreflang alternate the language. |
| `linkType` | `link` (`<a href>`), `image` (`<img src>`), `script`, `stylesheet`, `icon`, `canonical`, `alternate` (hreflang), `other` (another `<link href>`), `start URL` or `sitemap`. |
| `result` | `broken`, or with **Output all links** also `ok`, `restricted` (login or bot protection, could not be verified) and `not checked` (robots.txt, a limit, or links to other sites switched off). |
| `status` | HTTP status code of the final answer, such as `404` or `500`. Empty when no answer came. |
| `errorClass`, `error` | Why there was no usable answer: `dns`, `timeout`, `tls`, `connection_refused`, `connection_reset`, `redirect_loop`, `too_many_redirects` and others, with a sentence such as `The domain does not resolve in DNS.` |
| `isInternal` | `true` when the link points to the crawled site, with or without `www.`. |
| `nofollow` | `true` when the `<a>` tag has `rel="nofollow"`. |
| `finalUrl`, `redirectCount` | Where the link ends up and through how many redirects. A link that works only through a redirect is worth updating. |
| `change` | With a watch name: `newly broken`, `still broken` or `fixed` since the last run. |
| `checkedAt` | When the row was written, as an ISO 8601 UTC timestamp. |

### How to find broken links on a website

1. Enter the home page or any page in **Start URLs**, such as `https://www.example.com/`. To check exactly the pages of a sitemap, enter the sitemap instead, such as `https://www.example.com/sitemap.xml`; sitemap indexes and `.xml.gz` files are read too.
2. Set **Maximum pages per site**. The crawl goes breadth first, so the pages closest to the start come first. **Stay in the start folder** keeps it under one section such as `/docs/`, and **Maximum click depth** limits how far it goes.
3. Choose what to check: **Check images** is on by default, **Check scripts, stylesheets and head links** adds `<script src>`, stylesheets, icons, canonical and hreflang links, and **Check links to other sites** can be turned off for an internal-only audit.
4. Add texts to **Skip URLs containing** for links you never want requested, such as `/logout` or `/cart/add`.
5. Click **Start**. Broken links appear in the **Output** tab, where you can export them as CSV, Excel or JSON. Turn on **Output all links** for a full inventory of every link on every crawled page.

Over the API, an input looks like this:

```json
{
  "startUrls": ["https://www.example.com/"],
  "maxPages": 500,
  "checkImages": true,
  "checkScriptsAndStyles": false,
  "excludeUrls": ["/logout"],
  "watchName": "example-weekly"
}
```

### Example output

One row from a run on 11 October 2026, a missing image on the practice site:

```json
{
  "url": "https://the-internet.herokuapp.com/asdf.jpg",
  "result": "broken",
  "status": 404,
  "errorClass": null,
  "error": null,
  "foundOn": "https://the-internet.herokuapp.com/broken_images",
  "anchorText": null,
  "linkType": "image",
  "isInternal": true,
  "nofollow": false,
  "finalUrl": "https://the-internet.herokuapp.com/asdf.jpg",
  "redirectCount": 0,
  "change": null,
  "checkedAt": "2026-10-11T15:34:33.788Z"
}
```

### How much does it cost to check a website for broken links?

| | Free plan | Starter | Scale | Business |
|---|---|---|---|---|
| 1,000 links | $0.80 | $0.64 | $0.52 | $0.40 |

You pay only for the events in the table. There is no start fee, and compute time and proxies are included.

You pay per unique link checked, and every crawled page counts as one link because it is requested too. A crawl of 300 pages of the Python documentation found 13,878 links pointing to 2,155 unique addresses and checked 1,725 of them, which costs $1.38 on the Free plan and $0.69 on Business; the rest were on GitHub after it asked to slow down, or disallowed by robots.txt. A link that appears on every page, such as a menu item, is charged once. Links skipped by robots.txt, by **Skip URLs containing** or by a limit are not charged, and rows in the dataset cost nothing on top.

### Limits

- Only pages on the host of the start URL are crawled (`www.` and the bare domain count as one site). Links to other sites get one request each and are never crawled.
- A crawl covers up to 20,000 pages per site (**Maximum pages per site**) and up to 200,000 unique links per run (**Maximum links to check**, 5,000 by default), so a run cannot grow without bound. Internal pages beyond the page limit are still checked with a single request.
- robots.txt of the crawled site is respected for every page and every link on that site, for `User-agent: *` or for the product token of your **User agent**. A `Crawl-delay` is honored up to 20 seconds, with one request at a time. A robots.txt that cannot be read after three attempts (a 5xx answer or no answer) is treated as disallowing the whole site, as the robots.txt standard requires.
- Each host gets at most 3 parallel requests by default. A 429 answer pauses that host and slows it to one request at a time, and after five 429s its remaining links are `not checked`. Sites that rate-limit, such as GitHub, can make a run take minutes longer.
- A 502, 503 or 504 slows that host to one request per second, and the link is tried once more before it counts as broken.
- When a site answers with a bot protection challenge (Cloudflare, DataDome, Akamai, Imperva, PerimeterX, AWS WAF, Anubis, LinkedIn's 999), its links are `restricted` and no further requests go to it. The actor does not try to get around protection, and if the crawled site itself is protected, the crawl stops there.
- Links are read from the HTML the server sends. Links added by JavaScript after the page loads are not seen, because pages are not rendered.
- A link is tried with `HEAD` first and with `GET` when the server refuses `HEAD`. A link is broken on a 4xx or 5xx answer, a DNS error, a timeout (15 seconds by default), a TLS error, a refused or reset connection or a redirect loop. 401 and 407 count as `restricted`.
- Speed on 11 October 2026: the practice site prefilled in the form (18 pages, 85 links) took under a second of crawling, 30 sitemap pages of crawlee.dev with 534 links took 13 seconds, and 300 pages of the Python documentation with 1,725 links took under 3 minutes, most of it spent on links to GitHub, which allows only a slow pace.

### Watching a site for new broken links

Give the run a **Watch name**, or save the input as a task, and schedule it weekly or daily. Each broken link is then marked `newly broken` or `still broken`, links that were broken last time and work now are added as rows marked `fixed`, and a `CHANGES` record lists:

- `newlyBroken`: links broken now that were not broken in the previous run, with up to five pages they are on.
- `fixed`: links that were broken and now work.
- `stillBroken`: how many broken links are left from last time.
- `noLongerLinked`: broken links from last time that no page links to anymore. It is filled only after a run that crawled the whole site within its limits.

### FAQ

#### Is it legal to crawl a website for broken links?

The actor reads public pages the way a search engine crawler does, follows the site's robots.txt and keeps the request rate low, and it checks links on other sites with a single request each. It stores links, anchor texts and HTTP statuses, not page content. Check your own sites or sites you have permission to audit, and follow their terms.

#### Why is a link that works in my browser reported as restricted?

Some sites, such as LinkedIn and sites behind Cloudflare challenges, refuse requests from data centers. The actor marks those links `restricted` and moves on instead of guessing. The `SUMMARY` record lists every host that stopped answering normally.

#### Where do I see how much of the site was crawled?

The `SUMMARY` record in the key-value store has the pages crawled, links found and checked, broken links by link type and by reason (`404`, `500`, `dns`, `timeout`), pages left out at **Maximum pages per site**, pages disallowed by robots.txt and the robots.txt status of each host.

### Related scrapers

- [URL Status Checker](https://apify.com/deriverge/url-status-checker)
- [Tech Stack Detector](https://apify.com/deriverge/tech-stack-detector)
- [Article Extractor](https://apify.com/deriverge/article-extractor)

### Support

This actor is built and maintained by deriverge s.r.o., a software company based in the Czech Republic. If a run fails or a field you need is missing, please open an issue in the **Issues** tab or write to us at info@deriverge.com. We respond in English and Czech. Runs can be scheduled in Apify Console or started from the **API** tab, which has examples for Python, JavaScript and cURL and works with Make, Zapier, n8n and the Apify MCP server. If the actor saves you time, a short review helps other people find it.

# Changelog

This Actor's version history is a separate document: https://apify.com/deriverge/broken-link-checker/changelog.md

# Actor input Schema

## `startUrls` (type: `array`):

The page to start crawling from, such as https://www.example.com/, or a sitemap such as https://www.example.com/sitemap.xml to check exactly the pages it lists. The crawl stays on the host of the start URL (www. or not). Several sites can be listed, one per line.

## `maxPages` (type: `integer`):

How many pages of the site are crawled for links. Links on those pages are all checked, including internal pages beyond this limit, which get a single request each.

## `maxDepth` (type: `integer`):

How many clicks from the start page the crawl goes; 0 checks only the links on the start page. Empty means no depth limit.

## `stayInStartFolder` (type: `boolean`):

Crawl only pages under the folder of the start URL, for example only /docs/ when you start at https://www.example.com/docs/. Links to the rest of the site are still checked with one request each.

## `checkExternalLinks` (type: `boolean`):

Links to other websites are checked with one request each and never crawled. Off: only links within the site are checked.

## `checkImages` (type: `boolean`):

Checks the src of every <img> tag, so missing images show up as broken links of type image.

## `checkScriptsAndStyles` (type: `boolean`):

Also checks <script src> and <link href>: stylesheets, icons, canonical URLs and hreflang alternates.

## `outputAllLinks` (type: `boolean`):

Off: one row per broken link occurrence. On: one row for every link found on every crawled page, with its result (ok, broken, restricted, not checked), status and redirects, which makes a full link inventory of the site.

## `excludeUrls` (type: `array`):

Links whose address contains any of these texts are neither crawled nor checked, for example /logout, ?replytocom= or linkedin.com.

## `maxLinks` (type: `integer`):

The most unique URLs checked in one run (crawled pages count too), so the cost stays predictable. Links found after the limit are counted in SUMMARY as not checked.

## `timeoutSecs` (type: `integer`):

How long one link may take, redirects included, before it is reported as broken with the error class timeout.

## `maxConcurrency` (type: `integer`):

How many requests run in parallel across all hosts.

## `maxConcurrencyPerHost` (type: `integer`):

The most parallel requests one host gets, the crawled site included. A Crawl-delay in the site's robots.txt lowers this to one request at a time with that pause.

## `userAgent` (type: `string`):

Custom User-Agent header. robots.txt rules for its product token apply (for example MyBot/1.0 matches User-agent: MyBot). Empty means a current desktop Chrome user agent and the rules for User-agent: \*.

## `watchName` (type: `string`):

Name of the snapshot used to compare runs, for example "blog-links". Each broken link is then marked newly broken or still broken, links fixed since the last run are added as rows, and a CHANGES record lists them. A saved task gets a watch automatically.

## Actor input object example

```json
{
  "startUrls": [
    "https://the-internet.herokuapp.com/"
  ],
  "maxPages": 20,
  "stayInStartFolder": false,
  "checkExternalLinks": true,
  "checkImages": true,
  "checkScriptsAndStyles": false,
  "outputAllLinks": false,
  "maxLinks": 5000,
  "timeoutSecs": 15,
  "maxConcurrency": 20,
  "maxConcurrencyPerHost": 3
}
```

# Actor output Schema

## `links` (type: `string`):

One row per broken link occurrence (or per link with Output all links): the link, the page it is on, anchor text, link type, status and error class.

## `summary` (type: `string`):

Pages crawled, links found and checked, broken links by link type and by reason, links that could not be verified or were skipped, and robots.txt details per host.

## `changes` (type: `string`):

With a watch name or a saved task: newly broken links, links fixed since the previous run and broken links that are no longer linked.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        "https://the-internet.herokuapp.com/"
    ],
    "maxPages": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("deriverge/broken-link-checker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": ["https://the-internet.herokuapp.com/"],
    "maxPages": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("deriverge/broken-link-checker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    "https://the-internet.herokuapp.com/"
  ],
  "maxPages": 20
}' |
apify call deriverge/broken-link-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,deriverge/broken-link-checker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/vEVRVC0Fh0Ox3Mfft/builds/0KD6uM4ibkR53T9Mp/openapi.json
