# Broken Link Checker: Find 404s & Dead Links on Any Website (`brasspage/broken-link-checker`) Actor

Broken link checker and dead link finder: crawls a website, checks every internal and external link, and lists 404s, server errors, dead domains and redirects with the pages that link to them. Follows robots.txt. No browser, no proxy. $0.30 per 1,000 links.

- **URL**: https://apify.com/brasspage/broken-link-checker.md
- **Developed by:** [Brasspage](https://apify.com/brasspage) (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.30 / 1,000 link checkeds

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 Link Checker: Find 404s & Dead Links on Any Website

Give it a website. The Actor crawls the site's own pages, checks every link on them, internal and external, and lists the broken ones with the pages that link to them.

- **Finds every kind of dead link:** 404 and 410 pages, 5xx server errors, domains that no longer exist, timeouts and refused connections.
- **Shows where to fix it:** up to 5 pages that link to each broken URL, plus the link text.
- **Separates real breaks from bot walls:** 401, 403, 429 and LinkedIn's 999 are marked `blocked`, not `broken`, so you do not chase pages that only refuse robots.
- **Redirects too:** turn off **Only output broken and blocked links** to get every link with its final URL and redirect count.

**$0.30 per 1,000 links checked.** No browser, no proxy, no API key. External links get one GET that reads only the status and headers, so no page body is downloaded. The Apify free plan gets up to 2,000 links per run.

### How to use

1. Open the Actor in the Apify Console and go to the **Input** tab.
2. Type a website, like `https://crawlee.dev`, into **Websites**. One per line, up to 20.
3. Click **Start**.
4. When the run ends, open the **Output** tab. Download the broken links as CSV, Excel or JSON, or read them through the API.

The prefilled input crawls 5 pages of crawlee.dev and checks the first 100 links, in about a minute. Raise **Max pages to crawl per site** and clear **Max links to check** for a full site.

### What can you do with it?

- **SEO clean-up:** broken internal links waste crawl budget and leak link equity. Fix them from the `foundOn` list.
- **Site migrations and redesigns:** run it after a launch to catch old paths that now return 404.
- **Monitor a site:** schedule a weekly run and get only the new breaks (pair it with the dedup Actor below).
- **Agencies and freelancers:** check a client's site before a pitch or a handover.
- **AI agents:** call the Actor as a tool (Apify API or Apify MCP server) to audit a site's links.

### Input example

```json
{
    "startUrls": ["https://crawlee.dev"],
    "maxPagesPerSite": 200,
    "checkExternalLinks": true,
    "onlyBroken": true,
    "excludeUrlsContaining": ["/tags/", "?replytocom="],
    "maxLinks": 5000
}
```

### Output example

One row per broken or blocked link:

```json
{
    "url": "https://example.com/old-pricing",
    "result": "broken",
    "status": 404,
    "error": null,
    "finalUrl": "https://example.com/old-pricing",
    "redirects": 0,
    "internal": true,
    "foundOn": ["https://example.com/", "https://example.com/blog/launch"],
    "linkText": "See pricing",
    "startUrl": "https://example.com/"
}
```

`result` is one of `broken`, `blocked`, `redirected`, `ok` or `skipped` (a link to a private or local network address, never opened). `error` holds the network error for dead domains and timeouts, like `ENOTFOUND` or `TIMEOUT`. The `OUTPUT` record holds a summary: pages crawled, links checked, broken, blocked and redirected counts, and notes for each site.

### Input fields

| Field                   | What it does                                                                                   |
| ----------------------- | ---------------------------------------------------------------------------------------------- |
| `startUrls`             | Websites to check. Each is crawled from this URL, on its own host only (www is the same host). |
| `maxPagesPerSite`       | Pages opened and read for links (default 100, up to 5,000). Every link on them is checked.     |
| `checkExternalLinks`    | Check links to other sites, once each (default on). Other sites are never crawled.             |
| `onlyBroken`            | Output only `broken` and `blocked` links (default on). Off: every checked link.                |
| `excludeUrlsContaining` | Do not crawl or check URLs that contain one of these texts. Plain text, not a regex.           |
| `maxLinks`              | Stop after this many links in total. Crawled pages count as links.                             |

### Pricing

Pay per result: **$0.30 per 1,000 links checked**, plus a tiny start fee ($0.00005). Each unique URL is checked and charged once per run, even when many pages link to it. A link skipped by robots.txt or by your filters is free. The run stops at your maximum total charge, so a big site never costs more than you set.

The prefilled input checks at most 100 links (crawled pages count), so a one-click run costs about $0.03, plus the start fee.

### Polite and safe

- One request at a time per host, at most one a second, and slower if robots.txt asks for a `Crawl-delay` (up to 30 seconds; a site asking for more is skipped). A site with 500 internal links takes about 8 minutes.
- Follows robots.txt `Disallow` rules on the site it crawls.
- Only the start site is crawled. A link to another site gets one request, like a person clicking it.
- Private and local network addresses (including cloud metadata addresses) are never opened, on any redirect hop.
- Pages are capped at 5 MB, and the checker runs no page scripts.

### FAQ

**Why is a link `blocked` and not `broken`?** A 401, 403, 429 or LinkedIn's 999 answer usually means the site refuses robots, not that the page is gone. Open those links in a browser to be sure.

**Am I charged twice when many pages link to the same URL?** No. Each unique URL is checked and charged once per run, and `foundOn` lists up to 5 pages that link to it.

**Does it crawl other sites or subdomains?** No. It crawls only the start URL's host (www counts as the same host). Links to other sites and subdomains get one request each.

**How many links can I check on the Apify free plan?** Up to 2,000 links per run. A paid Apify plan has no such cap.

### Limits

- Up to 20 websites per run, and up to 5,000 pages crawled per site.
- Up to 2,000 links are read from one page, and up to 5 linking pages are kept per URL.
- Each request times out after 20 seconds and follows up to 5 redirects.
- It reads `<a href>` links in the HTML. Links built by JavaScript after the page loads are not seen, and neither are image, script or CSS URLs.
- Some sites answer bots with 403 or 429 even when the page works in a browser. Those show as `blocked`; open them by hand to be sure.
- Subdomains (like `blog.example.com` from `example.com`) are checked as links, not crawled. Add them as their own start URL to crawl them.

### Related Actors

- [Sitemap Extractor](https://apify.com/brasspage/sitemap-extractor): get every URL a site lists in its sitemaps.
- [Bulk Lighthouse & PageSpeed Audit](https://apify.com/brasspage/lighthouse-bulk-audit): audit the pages you crawled for Core Web Vitals.
- [Remove Duplicates & Merge Datasets](https://apify.com/brasspage/dataset-dedup-merge): with "only new items", get just the breaks found since the last run.

### Issues and support

Found a bug or need a feature? Open an issue in this Actor's **Issues** tab. We check it every day and reply there.

# Changelog

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

# Actor input Schema

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

One per line, up to 20. Each site is crawled from this URL, on its own host only (www counts as the same host).

## `maxPagesPerSite` (type: `integer`):

Pages on the site that are opened and read for links. Every link on those pages is checked. Requests to one host go one at a time, at most one a second, so a site with 500 internal links takes about 8 minutes.

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

Check each link to another site once, with a GET that reads only the status and headers. Other sites are never crawled.

## `onlyBroken` (type: `boolean`):

On: one row per broken link (404, 410, 5xx, dead domain, timeout) or blocked link (401, 403, 429, 999). Off: one row for every checked link, including ok and redirected ones.

## `excludeUrlsContaining` (type: `array`):

Optional. Do not crawl or check a URL that contains one of these texts, for example /wp-admin/ or ?replytocom=. Plain text, not a regex.

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

Stop after this many links in total (crawled pages count). Leave empty for no limit (the Apify free plan gets up to 2,000 links per run).

## Actor input object example

```json
{
  "startUrls": [
    "https://crawlee.dev"
  ],
  "maxPagesPerSite": 5,
  "checkExternalLinks": true,
  "onlyBroken": true,
  "excludeUrlsContaining": [
    "/wp-admin/",
    "?replytocom="
  ],
  "maxLinks": 100
}
```

# Actor output Schema

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

One row per checked link (by default only broken and blocked ones): url, result, status, error, final URL, redirects, and up to 5 pages that link to it.

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

Pages crawled, links checked, broken, blocked and redirected counts, and notes per site.

# 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"
    ],
    "maxPagesPerSite": 5,
    "maxLinks": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("brasspage/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"],
    "maxPagesPerSite": 5,
    "maxLinks": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("brasspage/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"
  ],
  "maxPagesPerSite": 5,
  "maxLinks": 100
}' |
apify call brasspage/broken-link-checker --silent --output-dataset

```

## MCP server setup

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