# Bulk PageSpeed and Core Web Vitals Checker (`opendatalabs/bulk-pagespeed-cwv-checker`) Actor

Audits a list of URLs with Google's official PageSpeed Insights API v5 and returns performance, accessibility, SEO and best-practices scores alongside lab and CrUX field Core Web Vitals.

- **URL**: https://apify.com/opendatalabs/bulk-pagespeed-cwv-checker.md
- **Developed by:** [Shyheim Ulrickson](https://apify.com/opendatalabs) (community)
- **Categories:** SEO tools, Developer tools, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.28 / 1,000 successful audits

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 and Core Web Vitals Checker

**Use it to** audit hundreds of URLs with Google PageSpeed Insights and get Lighthouse scores and real-user Core Web Vitals (including INP) in one table.

**Cost:** $1.50 per 1,000 audits (one audit = one URL and one strategy). **Requires your own Google PageSpeed Insights API key, which is free** (setup below takes about 2 minutes). A run without a key makes no API calls, returns one free guidance row, and costs only the $0.00005 start fee.

**Why this one**

- **Lab and field data in one row.** You get Lighthouse scores and timings plus 75th-percentile real-user metrics from Google's Chrome UX Report (CrUX) API, with page-to-origin fallback and the source recorded in `fieldDataSource`. Included in the audit price.
- **A bad URL never fails the run.** Timeouts, 500s and quota errors become rows with `errorKind`, and every other URL is still audited.
- **Quota-aware with your own key**, so you never share Google's exhausted anonymous quota. It can also chain straight from a sitemap run and audit only `added` or `changed` URLs.

**Sample output** (real run, Oct 20, 2026):

| url | strategy | performance | accessibility | seo | labLcpMs | fieldLcpMs | fieldInpMs |
|---|---|---|---|---|---|---|---|
| https://example.com/ | mobile | 100 | 96 | 80 | 757 | 1125 | 88 |

Audit hundreds or thousands of URLs with Google's official PageSpeed Insights API and get Lighthouse scores plus real-user Core Web Vitals in one dataset.

Feed it a URL list, or point it at the output of a sitemap Actor and audit a whole site. Each URL produces one row per strategy with performance, accessibility, SEO and best-practices scores, lab LCP/FCP/CLS/TBT, and CrUX field LCP/INP/CLS/FCP/TTFB where Google has enough real-user traffic.

### What it does

- **Uses the official API.** Google's PageSpeed Insights v5 endpoint. No headless browser, no scraping, no anti-bot problems.
- **Both lab and field data.** Lighthouse lab timings for every successfully audited URL, plus 75th-percentile CrUX field metrics (including **INP**, which only exists as field data). Where a page has too little traffic for URL-level CrUX, it falls back to origin-level and records which you got in `fieldDataScope`.
- **Per-URL errors, not a failed run.** A URL that times out, 500s, or trips a quota limit becomes a row with `ok: false`, `errorKind` and a readable `errorMessage`. Every other URL still gets audited.
- **Quota-aware.** A token-bucket rate limiter enforces your requests-per-minute ceiling. A per-minute throttle is retried with backoff; an exhausted **daily** quota is detected and the remaining URLs are written as skipped rows immediately, because a daily quota cannot clear inside a run.
- **Chains after a sitemap run.** Point `sourceDatasetId` at a sitemap Actor's dataset and audit every URL it found — or, in change-tracking mode, only the URLs that were `added` or `changed`.

### About the API key — read this first

**A key is required, and it is free.** Without one, PageSpeed Insights bills calls to a single Google-owned project shared by every anonymous caller in the world, and its daily quota is routinely already exhausted. In testing from a cloud IP, keyless calls returned this on the very first request:

```
Quota exceeded for quota metric 'Queries' and limit 'Queries per day'
of service 'pagespeedonline.googleapis.com'
for consumer 'project_number:583797351490'.
```

That is the shared project, not yours. Get your own key (free):

1. Open the Google Cloud Console and create (or pick) a project.
2. Enable the **PageSpeed Insights API**.
3. Enable the **Chrome UX Report API** in the same project (APIs & Services > Library > "Chrome UX Report API" > Enable). This powers the real-user (field) columns. It is also free.
4. Create an **API key** credential under APIs & Services > Credentials. If you restrict the key, allow both APIs.
5. Paste it into the `apiKey` input. It is marked secret, is only ever sent to `googleapis.com`, and is redacted from logs and error rows.

If the Chrome UX Report API is not enabled, audits still succeed: the field columns fall back to the data embedded in the PageSpeed Insights response, and each row carries `cruxError: "crux_api_not_enabled"` with a link to enable it. Lookups stop after the first such error, so the run doesn't keep calling a disabled API.

Keyed projects get **25,000 queries per day** and **240 queries per minute**. These are commonly cited limits; check your Quotas page in Google Cloud Console for the numbers that actually apply to your project.

**If you run it without a key**, the Actor makes **no** PageSpeed Insights calls. It returns one row with `ok: false` and `errorKind: "missing_api_key"` explaining how to get a key, charges no audit (only the $0.00005 start fee), and the run finishes as SUCCEEDED. Add the key and run again.

### Input

Minimum viable input:

```json
{
  "urls": ["https://example.com", "https://example.com/pricing"],
  "apiKey": "YOUR_PAGESPEED_API_KEY"
}
```

Mobile and desktop, with opportunity audits:

```json
{
  "urls": ["https://example.com"],
  "strategy": "both",
  "categories": ["performance", "accessibility", "best-practices", "seo"],
  "includeAudits": true,
  "apiKey": "YOUR_PAGESPEED_API_KEY"
}
```

Nightly audit of only what changed on a site, chained after the Sitemap URL Extractor:

```json
{
  "sourceDatasetId": "SITEMAP_RUN_DATASET_ID",
  "sourceDatasetField": "url",
  "sourceDatasetChangeTypes": ["added", "changed"],
  "maxUrlsFromSourceDataset": 500,
  "strategy": "mobile",
  "concurrency": 6,
  "maxRequestsPerMinute": 120,
  "apiKey": "YOUR_PAGESPEED_API_KEY"
}
```

#### Key settings

| Field | Default | Notes |
|---|---|---|
| `urls` | — | Pages to audit, or set `sourceDatasetId`. With neither, the run uses the demo URL `https://example.com` instead of failing. |
| `strategy` | `mobile` | `both` doubles the API calls and the quota used. |
| `apiKey` | — | **Required** (free). Without it the run returns one guidance row and makes no API calls. See above. |
| `categories` | all four | Quota is charged per call, not per category. |
| `includeFieldData` | `true` | Real-user (CrUX) metrics, where available. No extra charge. |
| `includeCrux` | `true` | Get field data from the CrUX API (page, then origin; PHONE for mobile, DESKTOP for desktop). Off = use only the field data inside the PSI response. |
| `includeAudits` | `false` | Adds Lighthouse opportunity audits; larger rows. |
| `concurrency` | `4` | A PSI call typically takes 10–45 s, so this is what makes a batch finish. |
| `maxRequestsPerMinute` | `60` | Hard ceiling. 240/min is the commonly cited keyed limit; check your Quotas page in Google Cloud Console. |
| `stopOnDailyQuotaExhausted` | `true` | Skip remaining URLs rather than retry into a wall. |
| `requestTimeoutSecs` | `120` | Below ~60 s you start timing out audits that would have passed. |

### Output

One row per URL and strategy:

```json
{
  "url": "https://example.com/",
  "finalUrl": "https://example.com/",
  "strategy": "mobile",
  "ok": true,
  "performanceScore": 74,
  "accessibilityScore": 91,
  "bestPracticesScore": 100,
  "seoScore": 92,
  "labLcpMs": 3605,
  "labFcpMs": 2184,
  "labCls": 0.041,
  "labTbtMs": 311,
  "labSpeedIndexMs": 3901,
  "hasFieldData": true,
  "fieldDataSource": "crux_url",
  "fieldDataScope": "url",
  "fieldLcpMs": 3120,
  "fieldLcpCategory": "needs-improvement",
  "fieldInpMs": 184,
  "fieldInpCategory": "good",
  "fieldCls": 0.12,
  "fieldOverallCategory": "needs-improvement",
  "lighthouseVersion": "12.2.1",
  "analysisTimestamp": "2026-10-19T00:11:22.333Z",
  "fetchedAt": "2026-10-19T00:11:40.901Z",
  "durationMs": 18234
}
```

A failed audit, in the same dataset:

```json
{
  "url": "https://example.com/slow-page",
  "strategy": "mobile",
  "ok": false,
  "errorKind": "quota_exceeded",
  "errorMessage": "PSI daily quota exhausted for this API key. Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'pagespeedonline.googleapis.com'.",
  "errorStatusCode": 429,
  "fetchedAt": "2026-10-19T00:12:03.441Z"
}
```

The dataset ships four views: **Scores**, **Core Web Vitals (field)**, **Lab metrics** and **Failed audits**. `RUN_SUMMARY` in the key-value store records totals, failures by kind, and whether the daily quota ran out.

### Use cases

- **Agency reporting.** Audit a client's top 500 pages on mobile and desktop and export straight to a spreadsheet.
- **Core Web Vitals monitoring.** Schedule a weekly run and track `fieldLcpMs`, `fieldInpMs` and `fieldCls` over time.
- **Pre- and post-release comparison.** Audit the same URL set before and after a deploy and diff the scores.
- **Competitive benchmarking.** Audit your key landing pages next to competitors' equivalents.
- **Site-wide audit from a sitemap.** Chain it after the Sitemap URL Extractor and audit everything, or only what changed.

### Limits and things to know

- **Quota is the real constraint.** Each URL-and-strategy pair is one API call. 1,000 URLs on `both` is 2,000 calls. Plan against your project's daily quota.
- **PSI is slow by nature.** 10–45 seconds per call is normal (heavy pages take longer). At the default concurrency 4 that is roughly 400–600 URLs per hour, so raise `concurrency` or the run timeout for bigger lists. Concurrency, not raw speed, is what gets a large batch done.
- **Lab numbers vary between runs.** Lighthouse runs on Google's infrastructure under variable load; small score movements between runs are noise, not regressions. Field (CrUX) data is the stable signal.
- **Not every page has field data.** CrUX needs enough real Chrome traffic. The Actor asks the CrUX API for the page, then for the whole origin (`fieldDataSource: "crux_origin"`). When CrUX has neither, the field columns stay empty, `hasFieldData` is `false` and `fieldDataSource` is `"none"`; the audit is still delivered.
- **Field data source.** `fieldDataSource` is `crux_url`, `crux_origin`, `psi_loadingExperience` (CrUX API off, or the lookup failed) or `none`. CrUX lookups are rate-limited to 100/minute, below Google's default of 150/minute per project.
- **INP is field-only.** Lighthouse has no lab INP for a single page load; `labTbtMs` is the lab proxy. `fieldInpMs` is the real thing.
- **Lighthouse can fail on a 200.** When the audited page itself errors, PSI returns HTTP 200 with a `runtimeError`. Those rows come back `ok: false` with `errorKind: "lighthouse:…"`.
- **Public pages only.** PSI audits what Google can reach anonymously; pages behind a login cannot be audited.
- **Review Google's API terms** for your own use of the output before relying on it commercially.

### Pricing events

| Event | Price | Charged when |
|---|---|---|
| Actor start | $0.00005 | Once per run (per GB of memory). |
| `audit` | $0.0015 ($1.50 per 1,000) | One URL was successfully audited, for one strategy. |

Nothing else is billable. Real-user CrUX data is part of the audit row and is never a separate charge. A quota error, a timeout, a URL skipped after the daily quota ran out, and a page Lighthouse could not load all produce a row so you can see what happened, but none of them is charged, because you received no audit. A run where every URL failed costs only the start fee, and so does a run without a key, whose single guidance row is free.

Auditing on `both` strategies charges twice per URL, because it is two Lighthouse runs.

When the run reaches your maximum total charge it stops cleanly: remaining URLs are written as skipped rows and the reason is recorded in `RUN_SUMMARY` rather than failing. Audits already in flight finish and are delivered free of charge, so a run can return slightly more results than it billed for, never fewer. `RUN_SUMMARY.charging` shows the units of work performed next to the units actually billed.

### Attribution

Results from Google PageSpeed Insights (Lighthouse); field data from the Chrome UX Report, CC BY 4.0. Not affiliated with or endorsed by Google. Requires your own Google API key.

This Actor ships no API key and has no built-in fallback key. The `apiKey` input is the only source, it is sent only to `googleapis.com`, and it is redacted from logs and error rows. Without one, the Actor makes no PageSpeed Insights calls at all and returns a single guidance row instead.

### FAQ

#### Do I need a Google API key?

Yes, and it is free. Create one in Google Cloud Console in about 2 minutes (steps above) and paste it into `apiKey`. Without a key the run makes no PageSpeed Insights calls, returns one free guidance row and finishes SUCCEEDED.

#### How do I audit a whole website?

Run a sitemap Actor first, then set `sourceDatasetId` to its dataset. Every URL it found is audited, or, in change-tracking mode, only the URLs that were added or changed.

#### Why do my scores change between runs?

Lighthouse lab scores vary with Google's server load, so small movements are noise. The real-user (CrUX) field metrics are the stable signal for trends.

#### Why are the field data columns empty for some pages?

Google only has real-user data for pages and sites with enough Chrome traffic. The Actor falls back from page to origin level and records the source in `fieldDataSource`; the lab audit is delivered either way.

#### Can I track Core Web Vitals every week and export them?

Yes. Schedule the Actor (Console > Schedules) and download each run as CSV, Excel or JSON, or send it to Google Sheets, Slack, Zapier, Make, n8n or a webhook with Apify's integrations.

### Feedback

This Actor is new and actively maintained. If a run fails, a field is missing, or a result looks wrong, please open an issue on the **Issues** tab with the run ID. We read and reply to every issue, and suggestions for new filters or fields are very welcome.

# Actor input Schema

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

Public page URLs. Each URL is audited once per selected strategy, and each audit is one row in the dataset. Bare domains are accepted and upgraded to https://. Leave this and the source dataset empty to audit the demo URL https://example.com.

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

Which form factor to emulate. 'Both' doubles the number of API calls, and therefore the quota used.

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

Required, and free. Enable both the "PageSpeed Insights API" and the "Chrome UX Report API" for the key's project. Without a key the run makes no PageSpeed Insights calls and returns one guidance row explaining how to get one (Google's shared anonymous quota is almost always exhausted). A free key from Google Cloud (enable the PageSpeed Insights API, then create an API key) gives you a private quota (commonly cited limits: 25,000 queries/day and 240/minute; check your Quotas page in Google Cloud Console). The key is only ever sent to googleapis.com and is redacted from logs.

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

Which Lighthouse categories to run. Fewer categories means a faster audit. Quota is charged per API call, not per category, so there is no quota saving in trimming this.

## `includeFieldData` (type: `boolean`):

Adds real-user Chrome UX Report metrics (75th percentile LCP, INP, CLS, FCP and TTFB) when Google has enough traffic for the URL. Falls back to origin-level data when URL-level data is missing; fieldDataScope and fieldDataSource record which one you got. Low-traffic pages have no field data at all, and hasFieldData will be false. Included in the audit price.

## `includeCrux` (type: `boolean`):

Looks up real-user data in Google's CrUX API with the same key (page first, then the whole site, matching the mobile/desktop strategy). Requires the free "Chrome UX Report API" to be enabled in the same Google Cloud project as your key. If it is not enabled or the lookup fails, the audit still succeeds and keeps the PageSpeed Insights field data; the row's cruxError says why. No extra charge. Turn off to use only the field data embedded in PageSpeed Insights responses.

## `includeAudits` (type: `boolean`):

Adds an 'audits' object with the main Lighthouse opportunities: render-blocking resources, unused JS and CSS, image formats, text compression, the LCP element and third-party weight. Makes rows noticeably larger.

## `locale` (type: `string`):

BCP-47 locale for Lighthouse's human-readable strings, for example en, de or pt-BR. Does not change the numbers.

## `sourceDatasetId` (type: `string`):

Dataset id or name to pull URLs from, instead of (or in addition to) the list above. Point this at a Sitemap URL Extractor run to audit a whole site without copying URLs by hand.

## `sourceDatasetField` (type: `string`):

Which property of each dataset item holds the URL.

## `sourceDatasetChangeTypes` (type: `array`):

When the source dataset came from the Sitemap Change Tracker in change mode, restrict the audit to URLs with these changeType values. Leave empty to audit every URL. Selecting added and changed is the cheap way to run a nightly audit of only what moved.

## `maxUrlsFromSourceDataset` (type: `integer`):

Safety cap. A sitemap run can easily return more URLs than your PSI quota allows in a day.

## `concurrency` (type: `integer`):

A single PSI call takes roughly 10 to 30 seconds, so concurrency is what makes a large batch finish. Capped at 1 when no API key is supplied. Defaults to 4 with a key.

## `maxRequestsPerMinute` (type: `integer`):

Hard rate ceiling, enforced by a token bucket. 240/minute is the commonly cited limit for a keyed project (check your Quotas page in Google Cloud Console); the default of 60 leaves headroom for anything else using the same key.

## `stopOnDailyQuotaExhausted` (type: `boolean`):

A daily quota does not reset inside a run, so once it is hit, every remaining URL is written as a skipped row immediately instead of spending the run's time on retries that cannot succeed. Turn this off only if you share a key with another workload that might free up quota mid-run.

## `maxRetries` (type: `integer`):

Total attempts, including the first. Retries back off exponentially with jitter and honour Retry-After. Per-minute rate limits are retried; a daily quota error is not.

## `requestTimeoutSecs` (type: `integer`):

PSI commonly takes 10 to 30 seconds and occasionally much longer on heavy pages. Below about 60 seconds you will start timing out audits that would have succeeded.

## Actor input object example

```json
{
  "urls": [
    "https://example.com"
  ],
  "strategy": "mobile",
  "categories": [
    "performance",
    "accessibility",
    "best-practices",
    "seo"
  ],
  "includeFieldData": true,
  "includeCrux": true,
  "includeAudits": false,
  "locale": "en",
  "sourceDatasetField": "url",
  "sourceDatasetChangeTypes": [],
  "maxUrlsFromSourceDataset": 1000,
  "concurrency": 4,
  "maxRequestsPerMinute": 60,
  "stopOnDailyQuotaExhausted": true,
  "maxRetries": 3,
  "requestTimeoutSecs": 120
}
```

# Actor output Schema

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

One row per URL × strategy with Core Web Vitals field data and Lighthouse lab metrics.

## `runSummary` (type: `string`):

Run summary: URLs and audits, succeeded/failed counts, daily-quota flag, stop reason, charged events and CrUX lookup status (crux.status).

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

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,opendatalabs/bulk-pagespeed-cwv-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/Q76AKWN7e44L9EcPN/builds/RCU8ooIDTUeFgFBe4/openapi.json
