# Broken Link Checker: 404s, Dead Links & Redirect Chains (`saif-tools/broken-link-checker`) Actor

Broken links only, without false alarms from bot-blocked sites. Checks every link, image, CSS and JS file on your site for 404s, 5xx, timeouts and redirect chains, with the page and anchor text for each. 403/429/999 are labelled access-denied. $0.0003 per link, $0.0008 per page.

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

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 links only, no false alarms.** This broken link checker crawls your website, checks every internal and external link, image, stylesheet and script, and returns one row per problem with the page it's on and the anchor text, so each fix takes a minute. Links that only fail because of bot protection (LinkedIn's 999, 403 or 429 responses) get their own `access-denied` label, so they don't land in your broken list.

- Finds 404s and other 4xx, 5xx errors, timeouts, DNS/SSL/connection failures, too many redirects and malformed URLs
- Flags **redirect chains** of 2+ hops
- Labels 401/403/429 and non-standard codes such as LinkedIn's 999 as **`access-denied`**, not broken
- Uses HEAD first, then confirms with GET before calling a link broken
- Respects robots.txt, waits between requests to the same host, stays on the start URL's host
- Plain HTTP, no browser: $0.0003 per unique link checked ($0.30 per 1,000 links), $0.0008 per page crawled

### Quick start

1. Click **Start**.
2. Enter your homepage in **Website(s) to audit**, e.g. `https://example.com`.
3. Keep the defaults (200 pages, depth 3, external links and images on).
4. Optional: set **Max cost per run**. The Actor stops cleanly when it is reached.
5. Run, then open the **Link issues** table, or export as CSV/Excel and sort by *Found on*.

```json
{
  "startUrls": ["https://example.com"],
  "maxPagesToCrawl": 200,
  "reportMode": "issues"
}
```

Only want hard failures? Use `"reportMode": "broken"`.

### Output

One row per link occurrence, so a dead link that appears on three pages gives three rows (each with its own `sourcePageUrl`) but is checked and charged only once. Three rows from our latest test crawl of the public docs site crawlee.dev (fields shortened):

```json
[
  {
    "linkUrl": "https://github.com//github.com/apify/crawlee/blob/f3eb99d9fa9a7aa0ec1dcb9773e666a9ac14fb76/packages/core/src/storages/sitemap_request_list.ts/issues/L446",
    "statusCode": 404,
    "isBroken": true,
    "issueType": "http-4xx",
    "redirectCount": 0,
    "anchorText": "/github.com/apify/crawlee/blob/f3eb99d9fa9a7aa0ec1dcb9773e666a9ac14fb76/packages/core/src/storages/sitemap_request_list.ts#L446",
    "sourcePageUrl": "https://crawlee.dev/js/api/core/changelog"
  },
  {
    "linkUrl": "https://apify.com/discord",
    "statusCode": 200,
    "isBroken": false,
    "issueType": "redirect-chain",
    "redirectCount": 2,
    "anchorText": "Discord channel",
    "sourcePageUrl": "https://crawlee.dev/blog"
  },
  {
    "linkUrl": "https://stackoverflow.com/questions/tagged/crawlee",
    "statusCode": 403,
    "isBroken": false,
    "issueType": "access-denied",
    "redirectCount": 0,
    "anchorText": "Stack Overflow",
    "sourcePageUrl": "https://crawlee.dev/"
  }
]
```

The first is a real malformed link (the domain is repeated in the path). The second works but passes through two redirects. The third is a working Stack Overflow page that blocks automated checks; it is **not counted as broken**.

Run summary for that crawl: **8 pages crawled, 998 unique links checked, about 5 minutes at the default politeness delay, cost ≈ $0.31.**

| `issueType` | Meaning | Counted as broken? |
|---|---|---|
| `http-4xx` / `http-5xx` | Server returned an error (404, 410, 500 …) | Yes |
| `timeout`, `dns`, `ssl`, `connection` | No usable response | Yes |
| `too-many-redirects`, `invalid-url` | Redirect loop or malformed URL | Yes |
| `redirect-chain` | Works, but goes through 2+ redirects | No (fix for speed and SEO) |
| `access-denied` | 401/403/429/999: usually bot protection | No (check manually) |
| `ok` | Only in `all` mode | No |

Per-site totals (pages crawled, links checked, issues) are saved in the key-value store as `OUTPUT`.

### Pricing

Pay per event. Output rows are free.

| Event | Price (USD) |
|---|---|
| Actor start | $0.00005 per GB of memory (default 512 MB = one event, $0.00005) |
| Page crawled (`page-crawled`) | $0.0008 |
| Unique link checked (`link-checked`) | $0.0003 |

Each unique URL is checked once per run, however many pages link to it. **Example from our test:** crawlee.dev, 8 pages and 998 unique links ≈ $0.31. The budget is checked before every page and every link, so "Max cost per run" is respected.

### Use cases

- Agencies: schedule a weekly run per client site and send the broken list to email or Slack (Apify schedules + Make, n8n or Zapier)
- Regular SEO and site-health audits
- Checking a site after a migration, redesign or URL change
- Docs and blog maintenance: find outdated external references
- An AI agent tool: "check this site for broken links"

### Input options

| Field | Default | What it does |
|---|---|---|
| `startUrls` | (required) | Start pages; the crawler stays on each start URL's host (subdomains are not followed) |
| `maxPagesToCrawl` | 200 | Internal HTML pages to scan per site (max 20,000) |
| `maxDepth` | 3 | Link depth from the start page (0 = start page only) |
| `checkExternalLinks` | true | Also check links to other domains |
| `checkImagesAndAssets` | true | Check `<img>`, stylesheets and scripts too |
| `reportMode` | `issues` | `issues` = broken + redirect chains + access-denied; `broken`; `all` |
| `excludePatterns` | \[] | Regexes for URLs to skip, e.g. `/logout` |
| `respectRobotsTxt` | true | Don't crawl pages robots.txt disallows |
| `maxConcurrency` | 5 | Parallel checks (max 20) |
| `requestDelayMs` | 250 | Delay between requests to the same host |
| `requestTimeoutSecs` | 20 | After this, the link is reported as `timeout` |

### Use with AI agents (MCP)

```
https://mcp.apify.com/?tools=saif-tools/broken-link-checker
```

Prompt: *"Check example.com for broken links and list the pages I need to fix."* For agent calls, use `reportMode: "broken"` and `maxPagesToCrawl: 50`.

### FAQ

#### How do I find broken links on my website?

Enter your homepage in **Website(s) to audit**, keep the defaults and run. Export the result as CSV and sort by `sourcePageUrl` to get a to-do list per page.

#### Why are some links "access-denied" rather than broken?

StackOverflow and npm returned 403, and LinkedIn returned 999, to our checker during testing, even though the pages work in a browser. Reporting them as broken would be a false alarm, so they get their own label. Open them by hand if in doubt.

#### What does a redirect chain mean?

The link works but passes through 2 or more redirects before reaching the page. Each hop adds latency and uses an extra crawl request. Update the link to the final URL.

#### Does it find links added by JavaScript?

No. It reads the HTML the server sends. Links that only appear after JavaScript runs (single-page apps) aren't found.

#### Will it crawl other websites?

No. It crawls only the start URL's host. External links get one lightweight request per unique URL to check their status.

#### How long does a run take?

It depends mostly on the per-host delay. Our crawlee.dev test (8 pages, 998 links, 250 ms delay) took about 5 minutes. Lowering `requestDelayMs` speeds it up; only do that on sites you control.

#### Can I run it every week and get the results in Slack or email?

Yes. Create an Apify schedule for the Actor, then send the dataset to Slack, email or Google Sheets with Apify's Make, n8n or Zapier integrations: https://docs.apify.com/platform/integrations

#### Can I audit any site?

Audit sites you own or have permission to audit.

### Related tools from saif-tools

**SEO toolkit:** run the [Sitemap URL Extractor](https://apify.com/saif-tools/sitemap-url-extractor) to list every URL your sitemaps publish, then this checker to find what's broken on the pages themselves. Together you see what you tell search engines exists and what actually works.

# Actor input Schema

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

Start pages, e.g. https://example.com. The crawler stays on each start URL's host.

## `maxPagesToCrawl` (type: `integer`):

Internal HTML pages to load and scan for links.

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

0 = only the start page, 1 = start page + pages it links to, ...

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

Also verify links pointing to other domains (one lightweight HEAD/GET per unique URL).

## `checkImagesAndAssets` (type: `boolean`):

Check <img src>, stylesheets and scripts, not just <a href> links.

## `reportMode` (type: `string`):

issues = broken links, redirect chains of 2+ hops and access-denied (bot-protected) links. broken = only broken links. all = every link occurrence (large).

## `excludePatterns` (type: `array`):

Links matching any regex are neither crawled nor checked, e.g. /logout or ?replytocom=.

## `respectRobotsTxt` (type: `boolean`):

Skip internal pages that robots.txt disallows for our user agent.

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

Total parallel link checks across all hosts.

## `requestDelayMs` (type: `integer`):

Politeness delay per host.

## `requestTimeoutSecs` (type: `integer`):

Links that do not respond in time are reported as 'timeout'.

## Actor input object example

```json
{
  "startUrls": [
    "https://crawlee.dev"
  ],
  "maxPagesToCrawl": 5,
  "maxDepth": 3,
  "checkExternalLinks": true,
  "checkImagesAndAssets": true,
  "reportMode": "all",
  "excludePatterns": [],
  "respectRobotsTxt": true,
  "maxConcurrency": 5,
  "requestDelayMs": 250,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

## `results` (type: `string`):

One row per broken or problematic link or image: status code, issue type, redirect chain, source page and anchor text.

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

Crawl summary: pages crawled, links checked and issue counts.

# 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://crawlee.dev"
    ],
    "maxPagesToCrawl": 5,
    "checkExternalLinks": false,
    "reportMode": "all"
};

// Run the Actor and wait for it to finish
const run = await client.actor("saif-tools/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://crawlee.dev"],
    "maxPagesToCrawl": 5,
    "checkExternalLinks": False,
    "reportMode": "all",
}

# Run the Actor and wait for it to finish
run = client.actor("saif-tools/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://crawlee.dev"
  ],
  "maxPagesToCrawl": 5,
  "checkExternalLinks": false,
  "reportMode": "all"
}' |
apify call saif-tools/broken-link-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,saif-tools/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/0NfKXKndW4pXOQpE6/builds/7GaP7fGAx77NHebft/openapi.json
