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

Crawl a website and check every link on its pages: internal, external and, if you like, images, scripts and stylesheets. Finds 404s, server errors, timeouts, DNS and SSL failures and redirect loops, with the page, link text and redirect chain. Pay per page checked.

- **URL**: https://apify.com/private_image_hg9/broken-link-checker.md
- **Developed by:** [Michael Shanker](https://apify.com/private_image_hg9) (community)
- **Categories:** SEO tools, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 page 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: 404s, Redirects & Dead Links

Give it your website and it crawls the pages, then checks **every link on every page**: links to your own pages, links to other websites, and if you like the **images, scripts and stylesheets** each page loads. You get one row per broken or problem link, with the page it's on, the link text, what went wrong, and where the redirect chain ended.

It finds:

- **Broken links:** 404 Not Found, 410 Gone and other 4xx answers.
- **Server errors:** 500, 502, 503 and other 5xx answers (retried before they're reported).
- **Dead domains and servers:** DNS failures (the domain doesn't exist), refused or dropped connections, and timeouts.
- **SSL problems:** expired, self-signed or wrong-host certificates.
- **Redirect problems:** redirect loops and chains longer than 10 hops.
- **Links that can never work for visitors:** links to `localhost`, private network addresses, or with no domain at all (`http://`).

**You pay per page checked, not per link.** A page with 300 links costs the same as a page with 3.

### Who it's for

- Site owners and SEO teams who want a list of broken links to fix, page by page.
- Documentation teams checking that links to other projects haven't rotted.
- Agencies running a quick link audit on a client site before a launch or migration.
- Developers who want a scheduled check (run it weekly with Apify Schedules and send the dataset to Slack or email).

### How it compares

| | This Actor | Typical browser-based link checker | Desktop crawler (e.g. free versions of SEO spiders) |
|---|---|---|---|
| What you pay for | Per page crawled ($2 per 1,000 pages), whatever the number of links | Usually compute time, which grows with page weight | A licence, or a free tier with a URL limit |
| Runs in a browser | No, plain HTTP: fast and light | Yes: renders JavaScript, slower and heavier | No |
| Links added by JavaScript after load | Not seen | Seen | Depends on settings |
| External links checked | Yes, every one, once per run | Varies | Yes |
| Images, scripts, stylesheets | Optional | Varies | Yes |
| Where the problem is | Page, link text, HTML element, final URL, redirect count | Varies | Yes |
| Runs on a schedule in the cloud | Yes (Apify Schedules, API, integrations) | Yes | No, your computer must be on |

### How it works

1. **Crawl:** it starts at each start URL and follows links to pages of the **same site** (`www.example.com` and `example.com` count as one site; other subdomains like `blog.example.com` are separate sites). It reads up to **Max pages per site**, breadth first, up to **Max clicks from the start page**. It obeys your site's **robots.txt** for every page it reads and every link on your site it checks, and honors a Crawl-delay.
2. **Check:** every link found on those pages is checked, wherever it points. Each link is checked **once per run** even if hundreds of pages have it: a `HEAD` request first, then a `GET` (without downloading the body) when `HEAD` fails or is refused, so servers that answer `HEAD` wrongly don't show up as broken. Redirects are followed by hand, so you see how many there were and where they ended.
3. **Report:** a row per problem link per page it appears on, so you know exactly where to fix it.

It is polite by design:

- At most **3 requests per second to any one host of your site** by default, page fetches and link checks together.
- At most **10 link checks per second to any other host** your pages link to (github.com, a partner site, a CDN). Those hosts get one HEAD request per link (plus a GET when needed), once per run, so this stays light even for a host linked from every page.
- Your own pages are fetched at most **2 per second**, and never faster than the **Crawl-delay** your robots.txt asks for (honored up to 10 seconds). Link checks on a host with a Crawl-delay wait the delay too, up to 1 second, so a long Crawl-delay doesn't turn a few hundred link checks into an hour. Page fetches go ahead of queued link checks, so the crawl keeps moving.
- A User-Agent that names it (`BrokenLinkCheckBot`), retries with back-off on 429 and 5xx answers (a host that answers 429 is also checked more slowly for the rest of the run, down to one request every 2 seconds, and listed in `SUMMARY` as `hostsSlowedBy429`), and no requests to local or private network addresses.

### Input

| Field | Default | What it does |
|---|---|---|
| **Websites to check** (`startUrls`) | | One start page per line: `https://example.com/`, `example.com/docs/`. Start URLs on the same site share its page limit. Request-list style objects (`{"url": "..."}`) work too. |
| **Max pages per site** (`maxPagesPerSite`) | 100 | Pages crawled and checked per site (1 to 10,000). This also caps the cost per site. |
| **Max clicks from the start page** (`maxDepth`) | 10 | 0 checks the start page's links only; 1 also crawls the pages it links to, and so on. |
| **Also check images, scripts and stylesheets** (`checkAssets`) | false | Also check `img` (src and srcset), `script`, stylesheet and icon `link`s, `video`, `audio`, `source`, `track`, `iframe` and `embed`. |
| **List working links too** (`reportAllLinks`) | false | Save a row for every link, including working ones (`status: ok`) and the ones robots.txt kept it away from (`not_checked`). |
| **Requests per second per host of your site** (`maxRequestsPerSecondPerHost`) | 3 | 1 to 10, for each host of the crawled site. Page fetches stay at 2 per second or less, and follow its Crawl-delay. |
| **Link checks per second per other host** (`maxRequestsPerSecondPerExternalHost`) | 10 | 1 to 20, for each host that is only checked, never crawled. |

Example input:

```json
{
  "startUrls": ["https://www.example.com/"],
  "maxPagesPerSite": 500,
  "maxDepth": 10,
  "checkAssets": true,
  "reportAllLinks": false,
  "maxRequestsPerSecondPerHost": 3,
  "maxRequestsPerSecondPerExternalHost": 10
}
```

### Output

One row per broken or problem link and the page it was found on (values illustrative):

```json
{
  "sourcePage": "https://www.example.com/blog/launch",
  "linkUrl": "https://partner.example.org/old-pricing",
  "anchorText": "see their pricing",
  "linkType": "external",
  "status": "broken",
  "httpStatus": 404,
  "error": "HTTP 404 Not Found",
  "finalUrl": "https://partner.example.org/pricing-2023",
  "redirectCount": 1,
  "element": "a",
  "site": "example.com",
  "checkedAt": "2026-11-03T18:02:11.000000+00:00"
}
```

- `linkType`: `internal` (a page of the same site), `external` (another site) or `asset` (an image, script, stylesheet and so on).
- `status`: what was found.
  - `broken`: a 4xx answer such as 404 or 410.
  - `blocked`: 401, 403, 429 or 999. The page needs a login or the server blocks automated checks; it **may still work in a browser**, so check these by hand.
  - `server_error`: a 5xx answer, after a retry.
  - `timeout`, `dns_error`, `ssl_error`, `connection_error`: the server couldn't be reached.
  - `too_many_redirects`: a redirect loop, or more than 10 redirects.
  - `local_address`: the link points to `localhost` or a private network address (it was not requested).
  - `invalid_url`: the link isn't a usable address.
  - With **List working links too**: also `ok`, and `not_checked` for links robots.txt keeps the checker away from, and for links on a site that kept refusing automated checks (HTTP 429) even when asked slowly. Big sites such as GitHub do this when a page links to them hundreds of times; those links aren't reported as broken, they're counted in `SUMMARY` (`linksNotCheckedRateLimited`, `hostsGivenUpAfter429`) so you can check them by hand.
- `httpStatus` is the final status after redirects, `finalUrl` where the redirects ended, and `redirectCount` how many there were.
- `anchorText` is the link's visible text (or the image's alt text, or its aria-label). `element` is the HTML tag the link came from (`a`, `area`, `img`, `script`, `link`...).
- If a start page itself can't be read, you get one row for it with `element: "start page"` and the reason, at no charge.

The run also saves a `SUMMARY` record (pages checked and charged, problem links by status, per-site counts, whether a site hit its page limit or the run hit your spending cap) and a `PAGES` record listing every page crawled with its depth and link counts. Download results as JSON, CSV or Excel, or send them to Google Sheets, Slack, Zapier, Make or n8n.

### Pricing

Pay per event: **$0.002 per page checked ($2 per 1,000 pages)**. A page is charged once it has been read (an HTML page that answered 200) and all of its links have been checked, whether or not anything was broken. Links themselves cost nothing, and neither do pages that couldn't be read, non-HTML files, or rows for unusable start URLs.

Set a maximum charge per run in the run options: the Actor never starts a page it can't pay for, stops cleanly, and says so in the log and the `SUMMARY`.

### Good to know

- **Plain HTTP, no browser.** Links that a page adds with JavaScript after it loads aren't seen. Most sites render their links in the HTML; single-page apps that don't will show few links.
- **Fragments aren't checked.** `page#section` is checked as `page`; whether `#section` exists isn't.
- **robots.txt.** Pages and links on your own site that robots.txt disallows for this checker are skipped. Links to other sites get a single `HEAD` (and a `GET` if needed) per hop, paced per host, without reading those sites' robots.txt.
- **Bot protection.** Some big sites (LinkedIn, some news and shop sites behind Cloudflare and similar) refuse automated requests. Those links come back as `blocked`, not `broken`.
- **Limits.** Up to 1,000 links are checked per page (the rest are counted in `SUMMARY`) and pages are read up to 5 MB. A link request waits up to 10 seconds for an answer (a timeout isn't retried), and one link gets at most 25 seconds of network time in all, redirects and retries included, so a slow or hanging server can't hold up the crawl. A page waits up to 30 seconds. Each page gets **10 minutes** for its link checks: links still waiting then (only on very slow or very link-heavy hosts) are counted in `SUMMARY` as `linksNotCheckedPastDeadline`, listed as `not_checked` when **List working links too** is on, and the page is saved and charged as usual. A page that is somehow still not done after 15 minutes is dropped with a log line, not charged.
- **Where the time went.** Each page's log line shows how long it took and the run clock. `SUMMARY` has the run's seconds, seconds per site, the Crawl-delay used per site, the time spent waiting for each host's pace (`gateWaitSeconds`, summed over all requests) and the hosts waited on most.
- **Non-web links** (`mailto:`, `tel:`, `javascript:`, `#top`, app links) are skipped.

### Responsible use

Use it on websites you own or manage, or with permission. It reads only public pages, sends no forms, logs into nothing, and collects no personal data: the output is about links, not people.

This Actor isn't affiliated with or endorsed by any of the websites it checks.

### More tools from the same developer

- [SEO Audit Tool: On-Page Checks, Crawl & Broken Links](https://apify.com/private_image_hg9/seo-audit-onpage)
- [Sitemap URL Extractor: All Site URLs, Lastmod & News](https://apify.com/private_image_hg9/sitemap-url-extractor)
- [Bulk URL Status Checker: Redirect Chain & Response Time](https://apify.com/private_image_hg9/bulk-url-status-redirect-checker)
- [Wayback Machine Scraper: Snapshots, Changes & Page Text](https://apify.com/private_image_hg9/wayback-machine-snapshots)
- [Website Screenshot & PDF: Full Page, Mobile, Batch URLs](https://apify.com/private_image_hg9/website-screenshot-pdf)
- [Domain WHOIS, DNS & SSL Checker: Expiry, SPF & DMARC](https://apify.com/private_image_hg9/domain-whois-dns-ssl-checker)

# Actor input Schema

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

One start page per line, like https://example.com/ or example.com/docs/. The crawl starts there and visits pages of the same site (www. and the bare domain count as one site). Links on those pages are checked wherever they point. Start URLs on the same site share its page limit.

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

How many pages of each site are crawled and have their links checked. You pay per page, so this also caps the cost per site.

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

0 checks only the start page. 1 also crawls the pages it links to, and so on. Links on the last level are still checked, just not crawled.

## `checkAssets` (type: `boolean`):

Check the files each page loads: images (src and srcset), scripts, stylesheets, icons, video, audio and iframes. They get linkType "asset".

## `reportAllLinks` (type: `boolean`):

Save a row for every link, including the ones that work and the ones robots.txt kept the checker away from. Off: only broken and problem links are saved.

## `maxRequestsPerSecondPerHost` (type: `integer`):

The most requests sent each second to any one host of the site being crawled, page fetches and link checks together. Page fetches are also kept to at most 2 per second and to the Crawl-delay its robots.txt asks for (up to 10 seconds); link checks there wait at least that delay, up to 1 second.

## `maxRequestsPerSecondPerExternalHost` (type: `integer`):

The most link checks sent each second to any one host your pages link to but that is not crawled (github.com, a partner site, a CDN...). Each link is checked once per run with one HEAD request (plus a GET when needed).

## Actor input object example

```json
{
  "startUrls": [
    "https://crawlee.dev/"
  ],
  "maxPagesPerSite": 20,
  "maxDepth": 10,
  "checkAssets": false,
  "reportAllLinks": false,
  "maxRequestsPerSecondPerHost": 3,
  "maxRequestsPerSecondPerExternalHost": 10
}
```

# Actor output Schema

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

One row per broken or problem link and the page it was found on (every link with reportAllLinks).

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

Pages checked and charged, problem links by type, per-site counts, and whether the spending cap was reached.

## `pages` (type: `string`):

Every page that was crawled, with its depth and how many links it had and how many were problems.

# 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": 20
};

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

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

```

## MCP server setup

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