# Bulk PageSpeed Auditor: Lighthouse and Core Web Vitals (`opendata-tools/bulk-pagespeed-auditor`) Actor

Audit a list of URLs with Google's official PageSpeed Insights API. Get Lighthouse scores, lab and real-user Core Web Vitals, and the top issues to fix, one row per URL and device. Failed audits are not charged.

- **URL**: https://apify.com/opendata-tools/bulk-pagespeed-auditor.md
- **Developed by:** [Open Data Tools](https://apify.com/opendata-tools) (community)
- **Categories:** Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$8.00 / 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 PageSpeed Auditor: Lighthouse and Core Web Vitals

This tool checks how fast and how well built a list of web pages is, using Google's official PageSpeed Insights service. For each page you get the four Lighthouse scores (Performance, Accessibility, Best practices and SEO, each out of 100), the main speed measurements, real-visitor Core Web Vitals where Google has them, and a short list of the biggest problems to fix. Paste in your URLs, choose mobile, desktop or both, and you get one row per page that you can sort, compare or export. You pay $0.008 for each page audited successfully on each device type, and failed audits are free.

#### Who it's for

- Agencies and freelancers auditing client sites, or a prospect's site before a pitch.
- Developers and site owners checking many pages at once instead of testing them one by one on the PageSpeed website.
- SEO teams tracking Core Web Vitals across key landing pages, product pages or templates.
- E-commerce and content teams comparing their pages with competitors' pages.

#### Why use it

- **Official Google data**: results come from Google's PageSpeed Insights API, the same engine as the PageSpeed Insights website. Your pages are not scraped.
- **Many URLs in one run**: add a list of pages and get every result in one table instead of testing them one at a time.
- **Four scores per page**: Performance, Accessibility, Best practices and SEO, each from 0 to 100. Leave out the categories you don't need.
- **Lab and real-visitor numbers**: lab measurements from a fresh test (such as Largest Contentful Paint and Total Blocking Time), plus Core Web Vitals from real Chrome users when Google has enough data for the page.
- **What to fix first**: up to 10 failed checks per page, sorted by the estimated time they would save.
- **Mobile, desktop or both**: choose one device type, or run both and get one row for each.
- **No charge for failures**: a page that can't be audited gets a row with the error message, and you are not charged for it. You pay $0.008 only for each successful audit. One bad URL never stops the run.
- **Stays within your budget**: set a maximum total charge for the run, and it stops auditing new URLs once that limit is reached.

#### How to use it

1. Open the Actor in Apify Console and add the pages you want to check under **URLs to audit**. Each entry must start with `http://` or `https://`.
2. Choose a **Strategy**: **Mobile** (the default), **Desktop**, or **Both**. Both gives you two rows per page and costs twice as much.
3. Optionally untick **Categories** you don't need. All four are selected by default.
4. Click **Start**. Each page takes Google a few seconds to test, so larger lists take a few minutes.
5. Open the **Output** tab. The overview shows the URL, device, status, the four scores and the key speed numbers for each page. Rows with status `error` include the reason. Export everything as JSON, CSV or Excel.
6. To track scores over time, create a schedule under **Schedules** (weekly, for example) with the same input, and add an integration such as Google Sheets, Slack, Zapier or Make, or a webhook, to send the results on.

#### Pricing

You pay $0.008 for each page audited successfully on each device type. Rows with status `error` are not charged. You can set a maximum total charge for a run, and the run stops auditing new URLs once it is reached.

Example: auditing 100 pages on mobile costs $0.80. Auditing the same 100 pages on both mobile and desktop gives 200 rows and costs $1.60. Running that mobile audit once a week costs about $3.20 over four weeks.

### API and reference

This section is for developers and AI agents. It lists every input and output field, an example record and code examples.

#### Summary

Sends each input URL to the Google PageSpeed Insights API v5 (`https://www.googleapis.com/pagespeedonline/v5/runPagespeed`) and writes one dataset row per URL and strategy. Each row contains the Lighthouse category scores (0 to 100), lab metrics, Chrome UX Report (CrUX) p75 field metrics when available, and up to 10 failed audits sorted by estimated savings. The Actor does not fetch the pages itself. Failed audits become `status: "error"` rows and are not charged. They do not fail the run unless no audit succeeded at all (for example when the shared daily quota is used up), in which case the run ends as Failed with the reason in the status message. `SUMMARY` is still saved.

#### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `startUrls` | array | required | URLs to audit, as `{"url": "..."}` objects (plain strings are also accepted). Only `http` and `https` URLs on a public domain or public IP address are valid. Exact duplicates are removed. An invalid URL produces an error row without calling the API. An empty list fails the run. |
| `strategy` | `mobile`, `desktop`, `both` | `mobile` | Lighthouse device profile. `both` produces two rows per URL, each charged. |
| `categories` | string\[] | all four | Any of `performance`, `accessibility`, `best-practices`, `seo`. Scores for unselected categories are `null`. |
| `maxConcurrency` | integer | `5` | 1 to 20. Number of PageSpeed API requests in parallel. Values outside the range are clamped. |
| `apiKey` | string (secret) | none | Optional. A Google API key with the PageSpeed Insights API enabled. Leave empty to use the key configured on the Actor, which is shared and limited to 1,000 audits per run; if Google's shared daily quota runs out, the run stops and asks you to add your own key. |

#### Output

One dataset row per URL and strategy.

| Field | Type | Description |
|---|---|---|
| `url` | string or null | URL as given in the input. `null` if an entry had no `url` value |
| `finalUrl` | string or null | URL after redirects, as reported by Lighthouse. `null` on error rows |
| `strategy` | string | `mobile` or `desktop` |
| `fetchedAt` | string | ISO 8601 UTC time of the audit (time of the attempt on error rows) |
| `performanceScore` | integer or null | 0 to 100 |
| `accessibilityScore` | integer or null | 0 to 100 |
| `bestPracticesScore` | integer or null | 0 to 100 |
| `seoScore` | integer or null | 0 to 100 |
| `lcpMs` | number or null | Lab Largest Contentful Paint, ms |
| `fcpMs` | number or null | Lab First Contentful Paint, ms |
| `clsScore` | number or null | Lab Cumulative Layout Shift |
| `tbtMs` | number or null | Lab Total Blocking Time, ms |
| `speedIndexMs` | number or null | Lab Speed Index, ms |
| `ttiMs` | number or null | Lab Time to Interactive, ms |
| `fieldLcpMs` | number or null | CrUX p75 Largest Contentful Paint, ms. `null` when Google has no field data |
| `fieldInpMs` | number or null | CrUX p75 Interaction to Next Paint, ms |
| `fieldClsScore` | number or null | CrUX p75 Cumulative Layout Shift |
| `fieldOverallCategory` | string or null | CrUX overall rating: `FAST`, `AVERAGE` or `SLOW` |
| `topIssues` | object\[] | Up to 10 failed Lighthouse audits (score below 0.9), sorted by `savingsMs` descending (a missing saving counts as 0), then by `score` ascending, then by `id`. The metric audits behind the lab fields (LCP, TBT and so on) are left out because they are measurements, not fixes. Empty on error rows |
| `topIssues[].id` | string | Lighthouse audit ID, for example `render-blocking-insight` or `unused-css-rules` |
| `topIssues[].title` | string | Audit title |
| `topIssues[].score` | number | Audit score, 0 to 1 |
| `topIssues[].displayValue` | string or null | Lighthouse summary, for example `Est savings of 128 KiB` |
| `topIssues[].savingsMs` | number or null | Estimated time saving, ms. Taken from the audit's overall savings, or else the largest time-based metric saving (CLS savings are ignored). `null` if Lighthouse gives none |
| `status` | string | `ok` or `error` |
| `errorMessage` | string or null | Reason when `status` is `error`, otherwise `null` |

Example record from a real run (Apify run on 21 October 2026, mobile). Values are as returned, except that `topIssues` is trimmed to the first 3 of its 10 entries. Lighthouse puts a non-breaking space before units in `displayValue`, shown here as `\u00a0`:

```json
{
  "url": "https://www.python.org",
  "finalUrl": "https://www.python.org/",
  "strategy": "mobile",
  "fetchedAt": "2026-10-21T06:17:41.491Z",
  "performanceScore": 83,
  "accessibilityScore": 75,
  "bestPracticesScore": 100,
  "seoScore": 92,
  "lcpMs": 3953,
  "fcpMs": 2721,
  "clsScore": 0.003,
  "tbtMs": 9,
  "speedIndexMs": 2721,
  "ttiMs": 3953,
  "fieldLcpMs": 1270,
  "fieldInpMs": 88,
  "fieldClsScore": 0,
  "fieldOverallCategory": "FAST",
  "topIssues": [
    {
      "id": "render-blocking-insight",
      "title": "Render-blocking requests",
      "score": 0,
      "displayValue": "Est savings of 2,590\u00a0ms",
      "savingsMs": 2600
    },
    {
      "id": "unused-css-rules",
      "title": "Reduce unused CSS",
      "score": 0,
      "displayValue": "Est savings of 128\u00a0KiB",
      "savingsMs": 620
    },
    {
      "id": "unused-javascript",
      "title": "Reduce unused JavaScript",
      "score": 0,
      "displayValue": "Est savings of 59\u00a0KiB",
      "savingsMs": 450
    }
  ],
  "status": "ok",
  "errorMessage": null
}
```

The run's default key-value store gets a `SUMMARY` record with `ok` (integer), `error` (integer), `keyless` (boolean, `true` if the run had no API key at all) and `stoppedEarly` (string or null, `"maximum total charge reached"` when the charge limit stopped the run).

Charge limit: when a charge reports that the run's maximum total charge has been reached, no new audits start. URLs not yet started are skipped and get no row. Audits already in progress at that moment finish and their rows are saved.

Error handling: invalid URLs fail without an API request. This includes missing URLs, schemes other than `http` and `https`, `localhost`, hosts without a dot, reserved names (`.invalid`, `.test`, `.localhost`, `.example`, `.local`, `.internal`) and private or reserved IP addresses. HTTP 429, 500, 502, 503 and 504 responses are retried, up to 4 attempts in total. The wait follows the API's `Retry-After` header (capped at 60 seconds), or else 2, 4 and 8 seconds. Two cases are not retried: a 429 for the daily quota, and a "Lighthouse returned error" message, which means the page itself failed to load. Other API errors also give an error row straight away.

#### Pricing details

Pay per event: `url-checked` at $0.008, charged once for each row with `status: "ok"`, so once per URL per strategy. Rows with `status: "error"` are not charged. The run stops starting new audits once the user's maximum total charge is reached.

#### Usage

Apify API (synchronous run, returns dataset items):

```bash
curl -X POST "https://api.apify.com/v2/acts/opendata-tools~bulk-pagespeed-auditor/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "startUrls": [
      {"url": "https://example.com"},
      {"url": "https://www.python.org"}
    ],
    "strategy": "mobile",
    "categories": ["performance", "seo"]
  }'
```

Python (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("opendata-tools/bulk-pagespeed-auditor").call(run_input={
    "startUrls": [
        {"url": "https://example.com"},
        {"url": "https://www.python.org"},
    ],
    "strategy": "both",
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["status"] == "ok":
        print(item["url"], item["strategy"], item["performanceScore"], item["lcpMs"])
    else:
        print(item["url"], item["strategy"], "error:", item["errorMessage"])
```

JavaScript (`npm install apify-client`):

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('opendata-tools/bulk-pagespeed-auditor').call({
    startUrls: [
        { url: 'https://example.com' },
        { url: 'https://www.python.org' },
    ],
    strategy: 'desktop',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) console.log(item.url, item.status, item.performanceScore, item.topIssues.length);
```

For recurring audits, create an Apify schedule with a fixed input and attach a webhook on `ACTOR.RUN.SUCCEEDED` to receive results.

#### Limits

- Results come from Google's API, so Lighthouse scores vary a little from run to run, as they do on the PageSpeed Insights website.
- Google can only test pages it can reach from the public internet. Pages behind a login, on a private network or blocking Google's tester return an error row.
- Each PageSpeed API request times out after 150 seconds (retries can add more). The default run timeout is 30 minutes, so very long lists may need a longer timeout or to be split into several runs.
- Field data (`field*` fields) comes from the Chrome UX Report and is `null` for pages without enough real-visitor traffic.

### FAQ

#### Do I need my own Google API key?

No. Leave **PageSpeed API key** empty and the Actor uses its own key. If you have a Google API key with the PageSpeed Insights API enabled, you can enter it, and requests then count against your own Google quota.

#### Why do some rows have status `error`?

The row's `errorMessage` gives the reason, for example an invalid or private URL, a page Google could not load, Google's daily quota being used up, or Google's API being busy after all retries. Error rows are not charged.

#### Why are the field data columns empty?

Real-visitor data comes from Google's Chrome UX Report, which only covers pages with enough Chrome traffic. Newer or low-traffic pages have no field data, so those fields are `null`. The lab scores and measurements are still filled in.

#### Does it crawl my site to find pages?

No. It audits exactly the URLs you give it. Add every page you want checked to **URLs to audit**.

#### What happens when my maximum total charge is reached?

The run stops starting new audits. Pages already being tested finish and are saved, the rest are skipped with no row, and the run's `SUMMARY` record shows `stoppedEarly`. Run again with the skipped URLs to finish the list.

#### Why is Both twice the price?

Mobile and desktop are separate Lighthouse tests with separate results, so each device type gives its own row and its own $0.008 charge. A page audited on both costs $0.016.

Issues and feature requests: use the Issues tab.

# Actor input Schema

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

Pages to audit. Only http and https URLs are accepted; invalid ones produce an error row.

## `strategy` (type: `string`):

Device profile used by Lighthouse. 'both' runs and charges each URL twice.

## `categories` (type: `array`):

Lighthouse categories to score. Scores for unselected categories are null.

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

Parallel PageSpeed API requests.

## `apiKey` (type: `string`):

Google API key with the PageSpeed Insights API enabled. If empty, env PSI_API_KEY is used; if that is also unset the run is keyless and the shared quota is very low.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://example.com"
    }
  ],
  "strategy": "mobile",
  "categories": [
    "performance",
    "accessibility",
    "best-practices",
    "seo"
  ],
  "maxConcurrency": 5
}
```

# Actor output Schema

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

All rows in the default dataset.

# 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": [
        {
            "url": "https://example.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("opendata-tools/bulk-pagespeed-auditor").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": [{ "url": "https://example.com" }] }

# Run the Actor and wait for it to finish
run = client.actor("opendata-tools/bulk-pagespeed-auditor").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": [
    {
      "url": "https://example.com"
    }
  ]
}' |
apify call opendata-tools/bulk-pagespeed-auditor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,opendata-tools/bulk-pagespeed-auditor"
        }
    }
}
```

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/NljV5ndu4wE57KhKh/builds/nKEpHqTMtWpiabdd1/openapi.json
