# Broken Link Checker: Find 404s, Dead Links & Broken Images (`fecklesspleb/broken-link-checker`) Actor

Broken link checker and 404 checker: crawl a website and find broken links and images (404s, 410s, 5xx errors, DNS failures, timeouts) with the page each one is on, the link text and the status code. Internal and external links. Healthy links are free. Respects robots.txt.

- **URL**: https://apify.com/fecklesspleb/broken-link-checker.md
- **Developed by:** [Scott Williams](https://apify.com/fecklesspleb) (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

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 Link Checker: find 404s and dead links on any website

Broken Link Checker crawls a website from a start page, checks every link and image it finds, and gives you a list of the broken ones: the page each one is on, the link text, the URL, and the HTTP status code (404, 410, 500 and so on) or the network error (DNS failure, refused connection, timeout). Use it as a dead link checker, a 404 checker, or a bulk link checker for internal and external links.

No browser, no proxy, no login. It obeys robots.txt and crawl delays.

**At a glance:** $0.003 per unique broken URL found ($3.00 per 1,000); a dead URL linked from many pages is charged once per run. Healthy links are free in the default mode. Pay per result with no subscription, set a maximum cost per run, export to JSON, CSV, Excel or HTML, or call it from the Apify API, n8n, Make or Zapier.

### Who it's for

- **SEO teams and agencies** cleaning up 404 errors and dead outbound links during site audits.
- **Site owners and developers** checking a site before and after a migration, redesign or URL change.
- **Content teams** finding dead links and missing images in old blog posts and documentation.
- **Anyone monitoring a site:** schedule a weekly run and get a fresh list of broken links each time.

### What counts as a broken link

- HTTP 4xx and 5xx responses, such as 404 Not Found, 410 Gone, 500 and 502.
- Network failures: DNS errors, refused connections, timeouts, too many redirects.

Not counted as broken:

- **401 and 403** (login walls and bot protection usually mean the page exists) and **429** (rate limited).
- **Links disallowed by the target site's robots.txt.** They are not requested and are counted as "not checked".

Each link is first checked with a HEAD request. If the server answers HEAD with 403, 404, 405, 501 or any 5xx, the link is checked again with GET before it is reported, because many servers handle HEAD badly.

### What it checks

- Every `<a href>` link on each crawled page, internal and (by default) external.
- Every `<img src>` image on each crawled page (turn off with **Check images**).
- Only `http` and `https` URLs. `mailto:`, `tel:`, `javascript:`, `data:`, `sms:`, `ftp:` and `#` links are skipped.

The crawler follows internal links on the same host (`www.` and the bare domain count as the same host) until it reaches **Max pages to crawl per site**. Links found on those pages are all checked, including internal pages it did not crawl.

### How to use it

1. Open the Actor in Apify Console and click **Try for free**.
2. Add one start URL per site in **Website URLs**, usually the home page.
3. Set **Max pages to crawl per site**. The default is 10; raise it to cover the whole site.
4. Optional: turn off **Check external links** or **Check images**, add **Exclude URL patterns**, or turn off **Only broken links** to get every link with its status.
5. Click **Start** and export the dataset to CSV or Excel. Each row tells you which page to edit and which link to fix.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `startUrls` | array of URL strings (required) | `["https://the-internet.herokuapp.com/broken_images"]` | Start page of each site. A URL without `http` gets `https://` added. |
| `maxPagesPerSite` | integer, 1 or more | 10 | How many pages of each site to crawl for links. |
| `checkExternalLinks` | boolean | true | Also check links that point to other websites. |
| `checkImages` | boolean | true | Also check image sources. |
| `onlyBroken` | boolean | true | Save only broken links. Turn off to save every checked link with its status. |
| `excludePatterns` | array of strings | none | Regular expressions. Matching links are neither crawled nor checked. |
| `maxResults` | integer | 0 | Stop after this many dataset rows. 0 means no limit. |

#### Example input

```json
{
  "startUrls": ["https://the-internet.herokuapp.com/broken_images"],
  "maxPagesPerSite": 500,
  "checkExternalLinks": true,
  "checkImages": true,
  "onlyBroken": true,
  "excludePatterns": ["/tag/", "\\?replytocom="]
}
```

### Output

One row per broken link occurrence. If the same dead URL is linked from 3 pages, you get 3 rows, one per page to fix, and pay for 1. This is a real row from a test run on the default start page:

```json
{
  "site": "https://the-internet.herokuapp.com/broken_images",
  "pageUrl": "https://the-internet.herokuapp.com/broken_images",
  "linkUrl": "https://the-internet.herokuapp.com/asdf.jpg",
  "linkText": null,
  "type": "image",
  "internal": true,
  "statusCode": 404,
  "isBroken": true,
  "error": "HTTP 404",
  "finalUrl": "https://the-internet.herokuapp.com/asdf.jpg",
  "redirected": false,
  "checkedAt": "2026-10-30T01:31:18.990180+00:00"
}
```

| Field | Description |
|---|---|
| `site` | The start URL of the crawl |
| `pageUrl` | The page the link was found on |
| `linkUrl` | The link or image URL |
| `linkText` | Anchor text for links, alt text for images (up to 120 characters) |
| `type` | `link` or `image` |
| `internal` | `true` when the link points to the same site |
| `statusCode` | HTTP status, or `null` for network errors and links not checked |
| `isBroken` | `true` for broken links |
| `error` | For example `HTTP 404`, a DNS or connection error, `not checked: robots.txt disallows it`, or `not checked: private, loopback and link-local addresses are not allowed` |
| `finalUrl`, `redirected` | Where the link ended up after redirects, and whether it redirected |
| `checkedAt` | When the link was checked (UTC) |

The `RUN_SUMMARY` record in the key-value store has, per site, pages crawled, links checked, broken links, links not checked, and a note when the crawl stopped at **Max pages to crawl per site**.

### Pricing

This Actor uses pay-per-event pricing. You pay for results, not for compute time.

| Event | Price |
|---|---|
| Broken link (each unique broken URL, default mode) | $0.003 |
| Link (one row, when **Only broken links** is off; rows for links not checked because of robots.txt are free) | $0.0005 |
| Actor start (charged by Apify, once per GB of run memory; 1 event at the default 1 GB) | $0.00005 |

Examples:

- A site with 10 unique broken URLs, default mode: 10 × $0.003 + $0.00005 = **about $0.03**, however many healthy links it has.
- A full link report with 2,000 link rows (**Only broken links** off): 2,000 × $0.0005 = **about $1.00**.
- A site with no broken links, default mode: **$0.00005** (only the start event).

In the default mode each broken URL is charged once per run: if the same dead URL is linked from many pages you get one row per page, but only the first is charged. With **Only broken links** off, every checked row is charged. Use **Max results** or **Maximum cost per run** to cap a run.

**Set a maximum cost per run.** In Apify Console set **Maximum cost per run**, or pass `maxTotalChargeUsd` through the API. The Actor stops cleanly when the limit is reached and the status message says so.

### Integrations and API

The Actor ID is `fecklesspleb/broken-link-checker` (in API URLs: `fecklesspleb~broken-link-checker`).

- **n8n:** use the Apify node, choose **Run an Actor** with this Actor ID, then read the dataset items and send new broken links to Slack, email or a ticket.
- **Make:** use the Apify module **Run an Actor**, then **Get Dataset Items**.
- **Zapier:** use the Apify app with **Run Actor**, then a dataset items step.
- **Schedules:** create an Apify schedule to check your site weekly, and a webhook to get notified when a run finishes.
- **AI agents:** call it through the Apify API, the Apify clients, or Apify's MCP server.

#### Run it with curl

```bash
curl -X POST "https://api.apify.com/v2/acts/fecklesspleb~broken-link-checker/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"startUrls": ["https://the-internet.herokuapp.com/broken_images"], "maxPagesPerSite": 50}'
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("fecklesspleb/broken-link-checker").call(
    run_input={"startUrls": ["https://the-internet.herokuapp.com/broken_images"], "maxPagesPerSite": 50},
    max_total_charge_usd=1,
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["pageUrl"], "->", row["linkUrl"], row["error"])
```

#### JavaScript

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

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('fecklesspleb/broken-link-checker').call({
    startUrls: ['https://the-internet.herokuapp.com/broken_images'],
    maxPagesPerSite: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map((r) => `${r.pageUrl} -> ${r.linkUrl} (${r.error})`));
```

### Polite by design

- Obeys robots.txt (including wildcards) and `Crawl-delay` for both crawling and checking.
- At most one request every 0.5 seconds per host.
- Each URL is checked once per run, even if it appears on many pages.
- On pages with `<meta name="robots" content="nofollow">`, links are checked but not crawled further.

### Limits

- It reads the HTML the server returns and does not run JavaScript. Links that only appear after JavaScript runs are not seen.
- It checks `<a href>` links and `<img src>` images. It does not check `srcset`, CSS background images, scripts or stylesheets.
- Each run is independent. To track changes over time, compare the datasets of two runs.
- On the Apify platform, links whose address is private, loopback or link-local (also after a redirect) are not requested. They are free, counted as "not checked", and shown only when **Only broken links** is off, with `not checked: private, loopback and link-local addresses are not allowed`. A start URL with such an address is not crawled; the dataset then gets one free row with `recordType: "status"`.

### Common searches this Actor answers

- broken link checker for a whole website
- find 404 errors on my site
- dead link checker with export to CSV
- check external links for dead pages
- find broken images on a website
- bulk link checker API
- check links after a website migration

### FAQ

**How do I find all broken links on my website?**
Put your home page in **Website URLs** and set **Max pages to crawl per site** high enough to cover the site. Every link and image on the crawled pages is checked, and only broken ones are saved by default.

**Does it find broken external links too?**
Yes, by default. Turn off **Check external links** to check only links within your own site.

**Can it find broken images?**
Yes. Image sources from `<img src>` are checked when **Check images** is on (the default). Image rows have `type: "image"`.

**Why are 401, 403 and 429 not counted as broken?**
They usually mean a login wall, bot protection or a rate limit, not a dead page.

**Why is a link marked "not checked"?**
The target site's robots.txt disallows automated access to it, so the Actor does not request it. These are counted in `RUN_SUMMARY` and shown in the dataset only when **Only broken links** is off.

**Can I get every link with its status code, not just broken ones?**
Turn off **Only broken links**. Every checked link occurrence is saved with its status, at $0.0005 per row.

**Can I check several websites in one run?**
Yes. Add one start URL per site. Each site gets its own entry in `RUN_SUMMARY`.

**Is it legal to check links on a website?**
The Actor reads public pages the way a browser or search engine does, obeys robots.txt and crawl delays, never logs in, and collects no personal data. Use it on sites you own or are allowed to audit.

### Related Actors

- [Website SEO Audit](https://apify.com/fecklesspleb/website-seo-audit): on-page SEO checks and a 0 to 100 score for every page of a site.
- [Website Metadata Extractor](https://apify.com/fecklesspleb/website-metadata-extractor): titles, meta tags, Open Graph and JSON-LD from a list of URLs.
- [Tech Stack Detector](https://apify.com/fecklesspleb/tech-stack-detector): which CMS, frameworks, analytics and hosting a website uses.

# Changelog

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

# Actor input Schema

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

Start page of each site to check. Pages on the same host are crawled.

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

Pages of the site to crawl for links.

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

Also check links to other websites.

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

Also check <img> sources.

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

Output only broken links. Each unique broken URL is charged once per run, however many pages link to it. Turn off to get every checked link with its status (charged per row).

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

Skip links matching any of these regular expressions.

## `maxResults` (type: `integer`):

0 = no limit.

## Actor input object example

```json
{
  "startUrls": [
    "https://the-internet.herokuapp.com/broken_images"
  ],
  "maxPagesPerSite": 10,
  "checkExternalLinks": true,
  "checkImages": true,
  "onlyBroken": true,
  "maxResults": 0
}
```

# Actor output Schema

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

No description

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

No description

# 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/broken_images"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fecklesspleb/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/broken_images"] }

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

```

## MCP server setup

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