# Bulk URL Status Checker: Redirect Chain & Response Time (`private_image_hg9/bulk-url-status-redirect-checker`) Actor

Check the HTTP status of up to 100,000 URLs: every redirect hop with its status and timing, the final URL, https check, key headers, plus page title, meta robots and canonical. Flags loops, long chains, 404s, timeouts and DNS errors. Pay per URL answered.

- **URL**: https://apify.com/private_image_hg9/bulk-url-status-redirect-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

$0.50 / 1,000 url 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

## Bulk URL Status Checker: Redirect Chain & Response Time

Give it a list of URLs, up to **100,000 per run**, and get one row per URL with:

- the **final HTTP status** (200, 301, 404, 500...) and a plain **result** (`ok`, `not_found`, `server_error`, `redirect_loop`, `timeout`, `dns_error`...),
- **every redirect hop**: its URL, status code, `Location` and response time in milliseconds,
- the **final URL**, the number of redirects, whether it **ends on https**, and the **total time**,
- the final response's **Content-Type, Content-Length, Last-Modified, Server and X-Robots-Tag** headers,
- for HTML pages: the **`<title>`, meta robots and `rel=canonical`**,
- a **`notes`** list that flags what usually needs fixing: redirect chains longer than one hop, no https, https-to-http downgrades, loops, 404s, server errors, timeouts, DNS and TLS failures, blocked answers (401, 403, 429, 999), `noindex`, and a canonical pointing elsewhere.

**You pay $0.50 per 1,000 URLs that got an answer**, whatever the answer (a 404 or a 500 is exactly what you wanted to know). URLs where no server answered at all (the domain doesn't exist, the connection timed out or was refused, the TLS certificate is broken) and invalid entries still get a row, at no charge.

### Who it's for

- **SEO teams and agencies** checking redirects after a site migration, cleaning up redirect chains, or auditing a list of backlinks, sitemap URLs or old campaign links.
- **Developers and ops** verifying that a list of endpoints, docs links or short links still resolve, on a schedule.
- **Content and data teams** cleaning a spreadsheet of URLs before an import: which are dead, which moved and where to.

### How it compares

| | This Actor | Typical small URL status checker in the Store |
|---|---|---|
| Price | $0.50 per 1,000 URLs answered, no start fee | Often $1 per 1,000, or a start fee plus compute |
| URLs with no answer (DNS, timeout, TLS) | Free | Usually charged like any other URL |
| Redirect chain | Every hop with status, Location and time | Varies; often only the final status |
| Loop and chain detection | Yes, with cookie-aware loops (a site that sets a cookie and redirects to itself is not a loop) | Varies |
| Title, meta robots, canonical | Yes, from the first 512 KB of HTML pages | Rarely |
| HEAD with GET fallback | Yes (servers that answer HEAD wrongly are retried with GET) | Varies |
| Polite to servers | Per-host rate limit, slows down on 429, robots.txt for page content | Varies |

### How it works

1. Each URL is requested with **HEAD** (light: no page download). If the server refuses or fails HEAD (some answer 405 or even 404 by mistake), it is asked again with **GET**, without downloading the body. Choose **GET only** for servers you know answer HEAD wrongly.
2. **Redirects are followed by hand**, up to 10 hops, so every hop is reported with its own status and time. A URL seen twice in a chain is a loop (unless the site set a cookie in between, which the next hop then sends back, like a browser would; cookies never leave one URL's chain). Redirects to local or private network addresses are never followed.
3. For a final page that is **HTML and answers 2xx**, the first 512 KB are read for the `<title>`, meta robots and canonical. **This is only done where the site's robots.txt allows this tool to read that page** (robots.txt is read once per site and cached). The status check itself does not depend on robots.txt: like any link checker, it only requests the URLs you supplied, once each.
4. Answers are **retried with back-off** on 429 and 5xx (429 up to 4 tries, 503 up to 3, other 5xx twice; `Retry-After` honored up to 10 seconds). Timeouts are not retried.

It is polite by design: at most **3 requests per second to any one host** by default, a host that answers 429 is slowed down for the rest of the run (down to one request every 2 seconds) and, if it keeps refusing, its remaining URLs are left unchecked (free rows with result `rate_limited`). Different hosts are checked in parallel, and a host with thousands of URLs in your list takes its turn with the others instead of holding them up. Local and private network addresses (localhost, 10.x, 192.168.x, `.internal`...) are never requested, by name or by what the domain resolves to.

### Input

| Field | Default | What it does |
|---|---|---|
| **URLs to check** (`urls`) | | One per line. `example.com/page` is checked as `https://example.com/page`; write `http://` to check the plain-http version. Request-list style objects (`{"url": "..."}`) work too. |
| **Or paste a list** (`urlsText`) | | URLs pasted from a spreadsheet or text file: one per line or separated by spaces or tabs. Combined with the list above. Repeats are checked once. |
| **Request method** (`method`) | `HEAD_THEN_GET` | `HEAD_THEN_GET` or `GET`. |
| **Follow redirects** (`followRedirects`) | true | Off: the first answer only; a redirect's target is in `hops[0].location`. |
| **Read page title, meta robots and canonical** (`readPageDetails`) | true | Off: status and headers only, fewer requests. |
| **User-Agent** (`userAgent`) | `tool` | `tool` sends `UrlStatusCheckBot/1.0`. `browser` sends a common desktop Chrome User-Agent, to see what a visitor gets from sites that answer bots with 403. |
| **Requests per second per host** (`maxRequestsPerSecondPerHost`) | 3 | 1 to 10. |
| **Timeout per request** (`timeoutSecs`) | 15 | 1 to 60 seconds. One URL with all its hops and retries gets at most 4 times this (at least 30 s). |

Example input:

```json
{
  "urls": ["http://github.com", "http://wikipedia.org", "https://example.com/old-page"],
  "urlsText": "https://example.org/a\nhttps://example.org/b",
  "method": "HEAD_THEN_GET",
  "followRedirects": true,
  "readPageDetails": true,
  "userAgent": "tool",
  "maxRequestsPerSecondPerHost": 3,
  "timeoutSecs": 15
}
```

### Output

One row per URL (values illustrative):

```json
{
  "position": 2,
  "inputUrl": "http://wikipedia.org",
  "url": "http://wikipedia.org/",
  "finalUrl": "https://www.wikipedia.org/",
  "finalStatus": 200,
  "statusClass": "2xx",
  "result": "ok",
  "redirectCount": 2,
  "endsOnHttps": true,
  "totalTimeMs": 212,
  "hops": [
    {"url": "http://wikipedia.org/", "method": "HEAD", "status": 301, "location": "https://wikipedia.org/", "timeMs": 61},
    {"url": "https://wikipedia.org/", "method": "HEAD", "status": 301, "location": "https://www.wikipedia.org/", "timeMs": 74},
    {"url": "https://www.wikipedia.org/", "method": "HEAD", "status": 200, "location": null, "timeMs": 77}
  ],
  "contentType": "text/html",
  "contentLength": 75011,
  "lastModified": "Mon, 02 Nov 2026 09:12:40 GMT",
  "server": "ATS/9.2.11",
  "xRobotsTag": null,
  "title": "Wikipedia",
  "metaRobots": null,
  "canonicalUrl": null,
  "pageInfo": "read",
  "notes": ["long_redirect_chain"],
  "error": null,
  "checkedAt": "2026-11-04T16:02:11.000000+00:00"
}
```

**`result`** is one of: `ok` (2xx), `redirect` (a 3xx that was not followed, or a 300/304), `not_found` (404, 410), `client_error` (other 4xx), `blocked` (401, 403, 429, 999: a login wall or bot protection; the URL may still work in a browser), `server_error` (5xx), `redirect_loop`, `too_many_redirects` (more than 10), `bad_redirect` (a redirect with no `Location`, or to something that isn't a web address), `timeout`, `dns_error`, `ssl_error`, `connection_error`, `rate_limited` (the host kept answering 429, so the URL was left unchecked), `local_address`, `invalid_url`.

**`notes`** holds the `result` when it isn't `ok`, plus any of: `long_redirect_chain` (more than one redirect), `not_https` (the final URL is plain http), `https_to_http` (the chain went from https to http), `redirect_not_followed`, `noindex` (in meta robots or `X-Robots-Tag`), `canonical_elsewhere` (the canonical is a different URL).

**`pageInfo`** says whether the title and tags were read: `read`, `not_html`, `robots_disallowed` (robots.txt doesn't allow this tool to read the page, or couldn't be read because the server answered 429 or 5xx), `not_read` (the GET for the page failed), `off`; empty when the final answer wasn't 2xx.

`position` is the URL's place in your input, so you can sort the results back into your order (URLs are checked in parallel). `finalStatus` is empty when the final URL got no answer. `totalTimeMs` is the sum of the hops' response times, without the tool's own pacing waits.

Download results as JSON, CSV or Excel (the overview view is spreadsheet-friendly), or send them to Google Sheets, Zapier, Make or n8n. The run also saves a `SUMMARY` record with counts by result, status class and note, median and 90th-percentile response times, the hosts slowed down by 429, and whether the spending cap was reached.

### Pricing

Pay per event: **$0.0005 per URL that got an HTTP answer ($0.50 per 1,000)**. No start fee. A URL counts as answered when a server gave any HTTP status for it, even if a later hop of its redirect chain then failed (for example a redirect to a dead domain: the redirect itself is a finding). URLs where no server answered at all, local addresses, invalid entries and URLs left unchecked because the host kept answering 429 are free. Set a maximum charge per run in the run options and the Actor stops cleanly when it's reached, before starting a URL it couldn't pay for.

### Good to know

- **Speed:** different hosts are checked in parallel (50 URLs at a time), each host at most 3 requests per second by default. A list of 10,000 URLs on 10,000 different sites takes a few minutes. 10,000 URLs on one site take about an hour at 3 requests per second with page details off (one request per URL), about two with them on (HTML pages need a second request); raise the per-host rate if it's your own site, and the run timeout (1 hour by default) for big lists.
- **What servers tell bots:** some sites (LinkedIn, some shops behind bot protection) answer automated requests with 403, 429 or 999 even when the page works in a browser. Those rows say `blocked`; try the browser User-Agent, or check them by hand.
- **HEAD vs GET:** a few servers give a different status to HEAD than to GET. Any HEAD answer of 400 or above is double-checked with GET, and a host whose HEAD answers turn out wrong is asked with GET for the rest of the run.
- **No JavaScript:** it checks what the server answers over HTTP. Redirects done by JavaScript or `<meta http-equiv="refresh">` are not followed.
- **robots.txt:** checking the status of the URLs you give it doesn't depend on robots.txt (it is the same single request a link checker or a browser makes), but reading page content does: titles, meta robots and canonicals are only read where the site's robots.txt allows `UrlStatusCheckBot` (or `*`).

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

### More tools from the same developer

- [Broken Link Checker: 404s, Redirects & Dead Links](https://apify.com/private_image_hg9/broken-link-checker)
- [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)
- [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

## `urls` (type: `array`):

One URL per line, like https://example.com/page or http://example.org. A URL without http:// or https:// is checked as https://. Repeated URLs are checked once. Up to 100,000 URLs per run, together with the pasted list below.

## `urlsText` (type: `string`):

Paste URLs from a spreadsheet or a text file: one per line, or separated by spaces or tabs. Quotes, <angle brackets> and a comma or semicolon at the end of a URL are removed. Combined with the list above.

## `method` (type: `string`):

HEAD then GET (default): a light HEAD request for each hop, and a GET when the server refuses or fails HEAD (some answer HEAD with 405 or 404 by mistake). GET only: always GET, for servers that answer HEAD wrongly; the page body is still not downloaded, except the start of HTML pages for their title.

## `followRedirects` (type: `boolean`):

Follow 301, 302, 303, 307 and 308 redirects, up to 10 hops, and report every hop. Off: report the first answer only (a redirect shows its target in hops\[0].location).

## `readPageDetails` (type: `boolean`):

For final pages that are HTML and answer 2xx, read the first 512 KB to get the <title>, meta robots and rel=canonical. Only done where the site's robots.txt allows this tool to read the page. Off: status and headers only (faster, fewer requests).

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

The tool's own User-Agent (UrlStatusCheckBot) names it honestly. Some sites answer bots differently (often 403); the browser option sends a common desktop Chrome User-Agent to see what a visitor gets.

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

The most requests sent each second to any one host. A host that answers 429 (too many requests) is slowed down further, down to one request every 2 seconds. Different hosts are checked in parallel.

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

How long to wait for a server's answer to one request. A URL with all its hops and retries may take up to 4 times this (at least 30 seconds) before it is reported as a timeout.

## Actor input object example

```json
{
  "urls": [
    "http://github.com",
    "https://www.wikipedia.org/",
    "http://example.com/",
    "https://www.python.org/nonexistent-page",
    "https://this-domain-does-not-exist.invalid/"
  ],
  "method": "HEAD_THEN_GET",
  "followRedirects": true,
  "readPageDetails": true,
  "userAgent": "tool",
  "maxRequestsPerSecondPerHost": 3,
  "timeoutSecs": 15
}
```

# Actor output Schema

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

One row per URL: final status, result, every redirect hop with its time, final URL, headers, title, meta robots, canonical and notes.

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

URLs checked and charged, counts by result, status class and note, response times, and whether the spending cap was reached.

# 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 = {
    "urls": [
        "http://github.com",
        "https://www.wikipedia.org/",
        "http://example.com/",
        "https://www.python.org/nonexistent-page",
        "https://this-domain-does-not-exist.invalid/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("private_image_hg9/bulk-url-status-redirect-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 = { "urls": [
        "http://github.com",
        "https://www.wikipedia.org/",
        "http://example.com/",
        "https://www.python.org/nonexistent-page",
        "https://this-domain-does-not-exist.invalid/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("private_image_hg9/bulk-url-status-redirect-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 '{
  "urls": [
    "http://github.com",
    "https://www.wikipedia.org/",
    "http://example.com/",
    "https://www.python.org/nonexistent-page",
    "https://this-domain-does-not-exist.invalid/"
  ]
}' |
apify call private_image_hg9/bulk-url-status-redirect-checker --silent --output-dataset

```

## MCP server setup

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