# PageSpeed Insights Bulk Audit — Lighthouse & Core Web Vitals (`aaronoptimus/pagespeed-insights-bulk-audit`) Actor

Audit many URLs with Google's official PageSpeed Insights API: Lighthouse scores, real-user Core Web Vitals (CrUX) next to lab metrics, mobile and desktop in one row, ranked fix suggestions, sitemap input and change tracking. Pay per audit plus a small start fee, no platform fees.

- **URL**: https://apify.com/aaronoptimus/pagespeed-insights-bulk-audit.md
- **Developed by:** [Aaron Tuttle](https://apify.com/aaronoptimus) (community)
- **Categories:** SEO tools, Developer tools
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 page audited (per device)s

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

Run **Google PageSpeed Insights on hundreds of pages at once** and get the results as clean JSON, CSV or Excel: the four **Lighthouse scores** (performance, accessibility, best practices, SEO), **real-user Core Web Vitals** from Chrome users (LCP, INP, CLS, FCP, TTFB) next to the **lab metrics**, **mobile and desktop side by side**, a ranked list of **what to fix** and the failed accessibility, SEO and best-practice checks for every page.

It uses **Google's official PageSpeed Insights API**, the same engine as pagespeed.web.dev, so the numbers match what you and your clients see there. No scraping, no browsers to babysit. You pay per audit plus a small start fee, with no platform fees, and failed audits are free.

### Why use this PageSpeed Insights bulk checker

- **Mobile and desktop in one row.** Compare both devices without merging spreadsheets.
- **Field data and lab data side by side.** Google's real-user data (the Chrome UX Report) decides your Core Web Vitals; Lighthouse lab data tells you why. You get both, plus a clear `coreWebVitals: "passed" | "failed" | "no-data"` verdict per device, and you're told whether the field data is for the page itself or its whole site.
- **A fix list, not just a score.** The biggest improvement opportunities first, each with its estimated time saving, plus the titles of every failed accessibility, SEO and best-practice check.
- **Whole sites in one go.** Paste a sitemap, a sitemap index, a `.xml.gz` sitemap or just the home page (we find the sitemap via robots.txt).
- **Monitoring built in.** Turn on *Compare with the previous run*, schedule the Actor, and every row tells you what changed and whether the page **regressed**.
- **Fair billing.** Pages that can't be audited (404s, DNS errors, pages that never load, non-HTML files) are delivered with the reason and **cost nothing**. Google's own hiccups are retried automatically.
- **No platform fees on top.** One small fee per run plus one per successful audit. Compute is included.

### What PageSpeed Insights data do you get?

One row per page (or one row per page and device, if you prefer). Per device:

| Field | Example |
|---|---|
| `performance`, `accessibility`, `bestPractices`, `seo` | 64 · 38 · 92 · 82 (0–100, like pagespeed.web.dev) |
| `coreWebVitals`, `coreWebVitalsScope` | `failed` · `url` (or `origin` when Google only has site-wide data) |
| `field` | Real users, 75th percentile: `lcpMs` 2900 (`needs-improvement`), `inpMs` 180 (`good`), `cls` 0.05 (`good`), `fcpMs`, `ttfbMs`, overall rating |
| `originField` | The same for the whole site |
| `lab` | Lighthouse: `fcpMs`, `lcpMs`, `tbtMs`, `cls`, `speedIndexMs`, `ttiMs`, `serverResponseMs`, `totalBytes`, `requestCount` |
| `opportunities` | e.g. "Render-blocking requests — est. savings 4,200 ms", biggest first, with LCP / FCP / TBT savings and bytes |
| `failedAudits` | e.g. accessibility: "Image elements do not have `[alt]` attributes"; seo: "Document does not have a meta description" |
| `finalUrl`, `lighthouseVersion`, `auditedAt`, `runWarnings` | Where the page ended up after redirects, and audit details |
| `change` (optional) | Score and metric differences since the previous run, `regressed`, `improved` |
| `fullReportUrl` (optional) | The complete PageSpeed Insights JSON for your own analysis |
| `status`, `error`, `errorCode` | Why an audit failed (not charged), e.g. `ERRORED_DOCUMENT_REQUEST` with "(Status code: 404)" |
| `pageSpeedWebUrl` | Opens the page in Google's own PageSpeed Insights web app, handy for client reports |

Every page you submit gets a row, so nothing silently disappears: if a run stops at your maximum cost, its timeout or your key's daily quota, the remaining pages are listed with `status: "not-audited"` and the reason (free).

<details>
<summary>Example row (shortened)</summary>

```json
{
  "position": 1,
  "url": "https://www.example.com/",
  "finalUrl": "https://www.example.com/",
  "status": "ok",
  "mobile": {
    "status": "ok",
    "performance": 64, "accessibility": 38, "bestPractices": 92, "seo": 82,
    "coreWebVitals": "failed", "coreWebVitalsScope": "url",
    "field": { "lcpMs": 2900, "lcpRating": "needs-improvement", "inpMs": 180, "inpRating": "good", "cls": 0.05, "clsRating": "good", "fcpMs": 1650, "ttfbMs": 912 },
    "lab": { "fcpMs": 5501, "lcpMs": 5651, "tbtMs": 145, "cls": 0, "speedIndexMs": 5501, "totalBytes": 612394, "requestCount": 4 },
    "opportunities": [
      { "id": "render-blocking-insight", "title": "Render-blocking requests", "savingsMs": 4200, "lcpSavingsMs": 4200, "fcpSavingsMs": 4200 },
      { "id": "unminified-javascript", "title": "Minify JavaScript", "savingsMs": 2000, "savingsBytes": 200006 }
    ],
    "failedAudits": { "accessibility": ["Buttons do not have an accessible name"], "seo": ["Document does not have a meta description"], "bestPractices": [] },
    "lighthouseVersion": "12.8.2"
  },
  "desktop": { "status": "ok", "performance": 94, "coreWebVitals": "passed", "coreWebVitalsScope": "origin", "…": "…" },
  "source": "input"
}
```

</details>

The run's `OUTPUT` record summarises the run: audits succeeded and failed, error codes, sitemaps read, and any pages not audited.

### How much does a bulk PageSpeed audit cost?

| Event | Price |
|---|---|
| Run start | $0.005 |
| Successful audit (one page on one device) | $0.003 ($3 per 1,000 audits) |

| Example | Approx. cost |
|---|---|
| 1 page, mobile + desktop | $0.011 |
| 100 pages, mobile + desktop | $0.605 |
| 1,000 pages, mobile only | $3.005 |

Failed audits are free. To cap any run, use Apify's *Maximum cost per run* setting; the Actor stops cleanly at the limit and lists the pages it didn't get to.

The price is the same on every Apify plan. Apify's $5 free plan is enough for about 1,600 audits (800 pages on mobile and desktop).

### Google API key: optional for small runs, free for big ones

**Small runs need no key.** Leave the field empty and the Actor uses its shared built-in key, limited to **25 pages per run** so it stays available for everyone.

**For bigger or scheduled runs, add your own free key** (2 minutes, once, no credit card). It gives you your own daily quota, not shared with anyone else:

1. Open Google's [PageSpeed Insights "Get started" page](https://developers.google.com/speed/docs/insights/v5/get-started), click **Get a Key**, sign in with any Google account and pick or create a project. Google creates the key and shows it.
2. Copy the key (it starts with `AIza`) into the **Google API key** field. That's it.

<details>
<summary>The longer way, or if "Get a Key" doesn't work for you</summary>

1. Open [console.cloud.google.com/apis/library/pagespeedonline.googleapis.com](https://console.cloud.google.com/apis/library/pagespeedonline.googleapis.com), create a project if asked, and click **Enable**.
2. Go to **APIs & Services → Credentials → Create credentials → API key** and copy it.
3. Optional: under the key's **API restrictions**, choose *Restrict key* → **PageSpeed Insights API**. Leave **Application restrictions** on *None*, because Apify calls the API from its servers.

</details>

Apify stores the key encrypted; this Actor sends it to Google in a request header and never logs it or writes it to your results. Save it once in an Apify **Task** (Actor → *Create task*) and you never paste it again.

If the key has a problem, the run stops straight away with a plain explanation of what to change (key not valid, API not enabled, wrong restrictions, daily quota used up), and nothing more is charged.

### How to run PageSpeed Insights in bulk

1. Paste your **page URLs**, one per line, and/or a **sitemap**.
2. For more than 25 pages, paste your own free **Google API key** (see above).
3. Choose **devices** (mobile + desktop by default) and, if you like, fewer **categories**.
4. Click **Start**, then download the results as JSON, CSV or Excel, or get them through the API. In CSV and Excel the nested fields become columns like `mobile/performance` and `mobile/field/lcpMs`.

A page takes 10–30 seconds per device inside Google; with the default 8 parallel audits, 100 pages on both devices finish in roughly 5–12 minutes, and the default 4-hour run timeout fits about 2,500 pages on both devices. If a run gets close to its timeout it stops cleanly and lists the pages it didn't reach in the `OUTPUT` record.

#### Monitoring over time

Turn on **Compare with the previous run** and schedule the Actor (Apify → Schedules), for example weekly. Every row then carries a `change` object and a top-level `regressed: true` when any score dropped 10 points or more or Core Web Vitals went from passed to failed (smaller moves are normal run-to-run variation, but you still see them in `change`). Use a different **History storage name** per client so their histories stay apart.

### PageSpeed Insights API: Python, JavaScript, Zapier, Make and AI agents (MCP)

One HTTP call that returns the results:

```bash
curl -X POST "https://api.apify.com/v2/acts/aaronoptimus~pagespeed-insights-bulk-audit/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://www.example.com/","https://www.example.com/pricing"],"apiKey":"YOUR_GOOGLE_API_KEY","devices":"mobile"}'
```

Python (`pip install apify-client`):

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("aaronoptimus/pagespeed-insights-bulk-audit").call(run_input={
    "sitemapUrls": ["https://www.example.com/"],
    "maxUrlsFromSitemaps": 50,
    "apiKey": "YOUR_GOOGLE_API_KEY",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["url"], row["mobile"] and row["mobile"]["performance"], row["mobile"] and row["mobile"]["coreWebVitals"])
```

JavaScript (`npm install apify-client`):

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('aaronoptimus/pagespeed-insights-bulk-audit').call({
    urls: ['https://www.example.com/'],
    apiKey: 'YOUR_GOOGLE_API_KEY',
    outputFormat: 'perDevice',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

**AI agents (Claude, Cursor, ChatGPT and other MCP clients):** add Apify's MCP server with this Actor as a tool, `https://mcp.apify.com?tools=aaronoptimus/pagespeed-insights-bulk-audit`, and ask for example *"Audit the pages in example.com's sitemap and list the ones failing Core Web Vitals with their top fix."*

Schedules, webhooks and integrations (Google Sheets, Slack, Zapier, Make, n8n) work from the Actor's **Integrations** tab, e.g. a weekly run that posts pages with `regressed: true` to Slack.

#### Tips

- **Field data first.** `coreWebVitals` and `field` come from real Chrome users and are what Google Search uses; the lab `performance` score is a diagnostic estimate.
- **Mobile is what Google ranks with.** Choose *Mobile only* to halve the cost when desktop doesn't matter to you.
- **Big lists:** keep *Parallel audits* at 8 to 10. Higher values mostly run into Google's per-minute limit (the Actor then pauses briefly, so nothing breaks, but it isn't faster).

> **What this tool doesn't do**
>
> - No pages behind a login or on localhost: Google's servers must reach the page.
> - No scraping of pagespeed.web.dev: it uses Google's official PageSpeed Insights API within its quotas.
> - Without your own key, 25 pages per run on the shared built-in key. Your own free key removes the cap.
> - No fixes applied: you get the ranked fix list and failed checks, not changed code.

### FAQ

**Why are my scores a few points different from yesterday?** Lab scores vary a little from run to run on every Lighthouse tool, pagespeed.web.dev included. Field data (Core Web Vitals) is a 28-day average and is stable.

**What does `coreWebVitalsScope: "origin"` mean?** Google doesn't have enough real-user visits for that exact page, so it shows the data for the whole site, just like pagespeed.web.dev does. `no-data` means Google has no real-user data for the site at all; lab data is still there.

**Is INP required to pass Core Web Vitals?** LCP, INP and CLS must all be "good". When a page has too few interactions for INP, the verdict uses LCP and CLS, as PageSpeed Insights does.

**Can I audit pages behind a login, or on localhost?** No. Google's servers load the page, so it has to be publicly reachable.

**What if Google's per-minute limit is hit?** All audits pause together for a minute and continue. Only if Google keeps refusing for about ten minutes does an audit fail as `RATE_LIMITED` (not charged).

**Why did an audit fail?** The row says why: for example `ERRORED_DOCUMENT_REQUEST (Status code: 404)`, `DNS_FAILURE`, `NOT_HTML`, or `NO_FCP` (the page painted nothing, often a bot-protection page). Failed audits are not charged.

**Why did a run without a key stop at 25 pages, or say the built-in key's daily quota is used up?** The built-in key is shared by everyone who runs the Actor without a key, so each run is capped at 25 pages and the key has a daily limit. Your own free key removes both limits.

**Is this allowed?** It uses Google's official, documented PageSpeed Insights API within Google's quotas, and never scrapes. For regular or large audits, use your own free key: it is yours, under your own Google account.

### Related tools

By the same developer: [Google Trends Scraper](https://apify.com/aaronoptimus/google-trends-scraper) for search interest over time and by region, [Google Autocomplete Scraper](https://apify.com/aaronoptimus/google-autocomplete-scraper) for long-tail keyword ideas, [Google Analytics 4 Exporter](https://apify.com/aaronoptimus/google-analytics-4-exporter) for your own GA4 reports as flat rows, [Google Ads Transparency Scraper](https://apify.com/aaronoptimus/google-ads-transparency-scraper) for the ads competitors run, [Google News Scraper](https://apify.com/aaronoptimus/google-news-scraper) for headlines by topic and city, [Google Jobs Scraper](https://apify.com/aaronoptimus/google-jobs-scraper) for job listings with salaries, [Google Hotels Scraper](https://apify.com/aaronoptimus/google-hotels-scraper) for hotel prices, [Google Flights Scraper](https://apify.com/aaronoptimus/google-flights-scraper) for airfare and price calendars.

### Support

Found a problem or need a field that's missing? Open an issue on the Actor's **Issues** tab. If the Actor saves you time, a short review helps others find it.

This Actor is an independent tool and is not affiliated with, endorsed by or sponsored by Google.

# Changelog

This Actor's version history is a separate document: https://apify.com/aaronoptimus/pagespeed-insights-bulk-audit/changelog.md

# Actor input Schema

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

Pages to audit, one per line. A bare domain like example.com is read as https://example.com/. Duplicates are skipped.

## `sitemapUrls` (type: `array`):

Audit the pages listed in a sitemap. Accepts sitemap.xml, sitemap indexes, .xml.gz files, robots.txt or just the site's home page (its robots.txt or /sitemap.xml is used). Reading sitemaps is free.

## `maxUrlsFromSitemaps` (type: `integer`):

Stop collecting sitemap pages after this many (all sitemaps together).

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

Leave empty for small runs: the shared built-in key audits up to 25 pages per run. For more, add your own free PageSpeed Insights key (about 2 minutes): open developers.google.com/speed/docs/insights/v5/get-started, click "Get a Key", sign in, copy the key (starts with AIza) and paste it here. It gives you your own daily quota. Apify stores it encrypted; this Actor never logs or saves it. Tip: save it in an Apify Task so you only paste it once.

## `devices` (type: `string`):

Mobile uses a mid-range phone on a slow 4G connection, desktop a fast connection, exactly like pagespeed.web.dev. Each device is one audit.

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

Scores to compute. All four cost the same; fewer can be a little faster. Lab metrics and fix suggestions need Performance. Real-user (Core Web Vitals) data is always included when Google has it.

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

Language of the audit titles and display values, e.g. en, de, fr, es, pt-BR, ja.

## `maxOpportunities` (type: `integer`):

How many improvement suggestions (with estimated time savings) to include per page and device, biggest first. 0 leaves them out.

## `outputFormat` (type: `string`):

One row per page with mobile and desktop side by side, or one row per page and device (easier to filter and chart).

## `trackChanges` (type: `boolean`):

For scheduled monitoring: remembers each page's scores in a named storage in your Apify account and adds a "change" object (score and metric differences) plus regressed: true when a score drops 10+ points or Core Web Vitals go from passed to failed.

## `historyStoreName` (type: `string`):

Name of the key-value store that keeps the previous scores. Use a different name per client or schedule to keep their histories apart.

## `saveFullReports` (type: `boolean`):

Also store the complete PageSpeed Insights JSON for every audit in the run's key-value store; each row gets a fullReportUrl. Useful for custom analysis; makes runs a little slower.

## `maxUrls` (type: `integer`):

Optional cap on the number of pages audited in this run (typed URLs first, then sitemap pages). Leave empty for no cap.

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

How many audits run at the same time. Each audit takes 10–30 seconds, so 8 parallel audits are fast and stay well inside Google's per-minute limit. If Google reports its per-minute limit, all audits pause briefly and continue.

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

Retries when Google's server errors or a page loads unreliably (for example the page painted nothing). Permanent problems such as DNS failures or 404 pages are not retried.

## Actor input object example

```json
{
  "urls": [
    "https://apify.com/",
    "https://developer.chrome.com/"
  ],
  "maxUrlsFromSitemaps": 100,
  "devices": "both",
  "categories": [
    "performance",
    "accessibility",
    "best-practices",
    "seo"
  ],
  "locale": "en",
  "maxOpportunities": 10,
  "outputFormat": "perUrl",
  "trackChanges": false,
  "historyStoreName": "pagespeed-audit-history",
  "saveFullReports": false,
  "maxConcurrency": 8,
  "maxRetries": 2
}
```

# Actor output Schema

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

No description

## `runSummary` (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 = {
    "urls": [
        "https://apify.com/",
        "https://developer.chrome.com/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("aaronoptimus/pagespeed-insights-bulk-audit").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://apify.com/",
        "https://developer.chrome.com/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("aaronoptimus/pagespeed-insights-bulk-audit").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://apify.com/",
    "https://developer.chrome.com/"
  ]
}' |
apify call aaronoptimus/pagespeed-insights-bulk-audit --silent --output-dataset

```

## MCP server setup

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

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/2egVVBeeIxhbPneLU/builds/QoCOmJd92iNHXlUsW/openapi.json
