Skip to content
Back to Resources
Guide

The PageSpeed Insights API: Automating Lighthouse

Skopx Team
August 21, 2026
17 min read

The PageSpeed Insights API runs Lighthouse on Google's infrastructure and hands you the entire report as JSON from one GET request to https://www.googleapis.com/pagespeedonline/v5/runPagespeed. A single call returns lab scores for performance, accessibility, best practices and SEO, plus real-user Chrome UX Report data for both the specific URL and its whole origin, which is the half of the response most teams never touch.

That combination is what makes the endpoint worth automating. You are not just scripting a score. You are getting a synthetic lab run and a 28-day field aggregate in the same payload, and the gap between those two numbers is usually where the real story lives. This guide covers the endpoint and its parameters, the quota math that determines how many URLs you can watch, what each category actually measures, how the CrUX block is structured and where it silently goes missing, and how to build a monitoring loop whose alerts you will still trust in three months.

What does the PageSpeed Insights API actually return?

The response is a single JSON object with a handful of top-level keys. Four of them matter:

  • lighthouseResult is the complete Lighthouse report: every category, every audit, the full timing breakdown, screenshots, and the stack packs. This is lab data, produced by one synthetic page load in a Google data center.
  • loadingExperience is Chrome UX Report field data for the exact URL you asked about. Real Chrome users, real networks, real devices, aggregated over a trailing 28-day window.
  • originLoadingExperience is the same field data aggregated across the entire origin rather than one URL.
  • analysisUTCTimestamp tells you when the lab run happened, which you need if you are storing history and want to reason about what changed and when.

There are also id (the final URL after redirects), kind, version (the Lighthouse version that produced the report), and a captchaResult field that most keyed callers can ignore.

The size is worth planning for. A full report with all categories requested is routinely several hundred kilobytes of JSON, and can pass a megabyte on heavy pages. If you are storing raw responses for a few hundred URLs daily, that adds up faster than people expect. Most monitoring systems extract a couple of dozen fields and either discard the rest or archive it compressed in object storage.

Inside lighthouseResult, the two branches you will read most are categories and audits. Each category carries a score between 0 and 1 (multiply by 100 for the familiar number) and an auditRefs array listing which audits feed it and with what weight. Each audit carries its own score, a numericValue, a displayValue string, and often a details object with the actual offending resources.

The endpoint, the parameters, and a first request

There is exactly one endpoint and it takes query parameters. No POST body, no batch mode.

GET https://www.googleapis.com/pagespeedonline/v5/runPagespeed
ParameterRequiredNotes
urlYesThe page to analyze. Must be publicly reachable and URL-encoded.
keyPractically yesYour Google Cloud API key. Without it you get a tiny anonymous allowance.
strategyNomobile or desktop. The default is desktop, which surprises people.
categoryNoRepeatable. performance, accessibility, best-practices, seo. Defaults to performance only.
localeNoLocale for the human-readable strings in the report, for example en_US.
utm_source, utm_campaignNoFree-form attribution tags echoed back. Useful for tagging which job made the call.

A minimal request with curl:

curl -s -G "https://www.googleapis.com/pagespeedonline/v5/runPagespeed" \
  --data-urlencode "url=https://example.com/pricing" \
  --data-urlencode "strategy=mobile" \
  --data-urlencode "category=performance" \
  --data-urlencode "category=seo" \
  --data-urlencode "key=$PSI_KEY"

Note the repeated category parameter. That is how you request more than one, and it is a common source of confusion because a comma-separated list does not work the way people assume.

Two defaults deserve emphasis. First, strategy defaults to desktop, so if you never set it you have been measuring the easier of the two profiles while Google's indexing works from mobile. Always set it explicitly. Second, category defaults to performance alone, so an accessibility or SEO score you expected to find will simply be absent from categories rather than returning as null.

Latency is the other thing to design around. A single call typically takes ten to thirty seconds because a real Lighthouse run is happening on the other end. Requesting all four categories pushes it longer. Set your HTTP client timeout to sixty seconds or more, and never call this endpoint synchronously inside a user-facing request path. It belongs in a queue or a scheduled job.

How much can you call it? Quotas, keys, and rate limits

Without an API key you get an unkeyed allowance that is only suitable for kicking the tires. Any real automation needs a key, which you create in the Google Cloud console: make or pick a project, enable the PageSpeed Insights API in the API library, then generate an API key under credentials. Restrict the key by IP address if it lives on a server, because an unrestricted key in a client bundle is an invitation to burn your quota for you.

The documented quota for a keyed project is 25,000 queries per day and 240 queries per minute. Both numbers are per project, not per key, so creating three keys inside one project does not triple anything. If you genuinely need more, the request goes through a quota increase in the Cloud console rather than through any paid tier toggle.

The daily number is more generous than it sounds and the per-minute number is tighter than it sounds. Work the math for your own footprint:

FootprintCalls per runRuns per dayDaily total
50 URLs, mobile only50150
50 URLs, mobile and desktop1001100
200 URLs, both strategies4001400
200 URLs, both strategies40041,600
2,000 URLs, mobile only2,00012,000

Almost nobody hits 25,000 a day by monitoring their own site. What people do hit is 240 per minute, by firing an unthrottled Promise.all across a large URL list. Because each call takes twenty seconds or so, high concurrency also means a lot of simultaneous open sockets. A worker pool of five to ten concurrent requests is a sane default: it stays far under the per-minute ceiling, keeps memory predictable, and finishes a few hundred URLs in a reasonable window.

Handle the failure modes deliberately:

  • 429 means you exceeded the rate limit. Back off exponentially with jitter and retry. Do not retry immediately in a tight loop.
  • 400 usually means a malformed or unreachable URL. Do not retry these. Log them and move on, because the URL itself is the bug.
  • 500 with a message like ERRORED_DOCUMENT_REQUEST or NO_FCP means Lighthouse itself failed on that page. These are often transient: a slow origin, a redirect loop, a page that never painted. Retry once or twice with a delay, then record the failure as a data point rather than pretending the run did not happen.

That last habit matters more than it looks. A URL that intermittently fails to complete a Lighthouse run is telling you something real about its reliability. If your job silently swallows errors, you lose that signal entirely. The same discipline applies to any scheduled data collection, which is a theme covered further in our guide to technical SEO automation.

Categories: performance, accessibility, best practices, and SEO

Four categories are available on current Lighthouse versions. The Progressive Web App category was retired in Lighthouse 12, so treat it as gone rather than trying to request it.

Performance is the weighted composite everyone quotes. On Lighthouse 10 and later, the weights are:

MetricAudit idWeight
Total Blocking Timetotal-blocking-time30%
Largest Contentful Paintlargest-contentful-paint25%
Cumulative Layout Shiftcumulative-layout-shift25%
First Contentful Paintfirst-contentful-paint10%
Speed Indexspeed-index10%

Time to Interactive was dropped from the scoring formula in Lighthouse 10, though the audit may still appear in the report. The practical consequence of these weights is that main-thread work dominates. Total Blocking Time plus Largest Contentful Paint account for 55% of the score, and both are usually driven by JavaScript execution and render-blocking resources rather than by image weight alone.

Accessibility is a set of automated axe-core checks. It is genuinely useful and genuinely incomplete: a 100 here means no automated check failed, not that the page is usable with a screen reader. Treat it as a floor.

Best practices covers mixed content, browser console errors, deprecated APIs, image aspect ratios, and source map availability. It is the category most likely to move for reasons unrelated to anything your team shipped, because it reacts to third-party scripts throwing errors.

SEO is a thin, mechanical checklist: title, meta description, crawlability, valid hreflang, legible font sizes, descriptive link text. It will not tell you whether your content deserves to rank. For that layer of judgment you want a separate on-page audit, and our breakdown of what to look for in an SEO audit tool covers where the Lighthouse checklist stops and real analysis starts.

Requesting all four categories in one call is cheaper than four separate calls in quota terms, since it counts as one query. It does make the run slower and the payload bigger. If you only care about performance day to day, run performance-only daily and pull the full set weekly.

Lab versus field: the CrUX split inside one response

This is the part of the response that separates a useful monitoring system from a scoreboard.

lighthouseResult is lab data. One page load, one simulated device, one throttled connection, one moment in time, from a Google data center. It is reproducible enough to debug with, and it tells you what a page does under a controlled load.

loadingExperience and originLoadingExperience are field data from the Chrome UX Report: aggregated measurements from real Chrome users who opted into reporting, over a trailing 28-day window, expressed as the 75th percentile. Field data tells you what users actually experienced. It is the data Google references for the Core Web Vitals assessment.

DimensionLab (lighthouseResult)Field (loadingExperience)
SourceOne synthetic run in Google's cloudReal Chrome users who opted in
WindowA single momentTrailing 28 days
StatisticA single measurement75th percentile plus a three-bucket distribution
AvailabilityAny reachable public URLOnly URLs with enough traffic
Reacts to a fixImmediatelyGradually, over weeks
Good forDebugging and regression detectionJudging real user experience

The metric keys inside the field block are LARGEST_CONTENTFUL_PAINT_MS, INTERACTION_TO_NEXT_PAINT, CUMULATIVE_LAYOUT_SHIFT_SCORE, FIRST_CONTENTFUL_PAINT_MS, and EXPERIMENTAL_TIME_TO_FIRST_BYTE. First Input Delay was retired in 2024; Interaction to Next Paint is the responsiveness metric now, so any parser still looking for FIRST_INPUT_DELAY_MS is reading a field that no longer arrives.

Each metric object contains a percentile (the p75 value), a distributions array of three buckets with a proportion for each, and a category of FAST, AVERAGE or SLOW. The block as a whole carries an overall_category.

Three gotchas will bite you if nobody warns you:

  1. CLS is multiplied by 100. A percentile of 8 means a Cumulative Layout Shift of 0.08. If you store the raw integer and compare it against the 0.1 threshold, every page looks catastrophic.
  2. loadingExperience can be absent entirely. URL-level CrUX requires enough traffic to anonymize. Most pages on most sites do not qualify. When it is missing, fall back to originLoadingExperience and label the number honestly in your dashboard, because origin-level data describes the site and not the page.
  3. The distributions matter more than the percentile. A p75 LCP of 2.4 seconds sitting just inside the good threshold, with 22% of loads in the poor bucket, is a different situation from the same p75 with 4% poor. Store the buckets, not only the headline.

The thresholds you compare against, all at p75:

MetricGoodNeeds improvementPoor
Largest Contentful Paintup to 2.5sup to 4.0sover 4.0s
Interaction to Next Paintup to 200msup to 500msover 500ms
Cumulative Layout Shiftup to 0.1up to 0.25over 0.25
Time to First Byteup to 800msup to 1.8sover 1.8s

If field data is your main interest and Lighthouse is incidental, there is also a standalone CrUX API at chromeuxreport.googleapis.com with its own key, its own quota, and support for form-factor and connection-type dimensions that the PageSpeed response does not expose. Our guide to Core Web Vitals monitoring goes deeper on choosing between the two and on interpreting a 28-day rolling window without fooling yourself.

Why do two runs of the same URL give different scores?

Because they are different runs. Lighthouse in this environment uses simulated throttling: the page is loaded on a mid-tier Android device profile over a throttled connection, and some metrics are modeled rather than directly measured. Network conditions on Google's side, origin response time, A/B tests, third-party script timing, and ad auctions all vary between runs.

A three to five point swing on the performance score for an identical page is normal. Swings of ten points or more happen on script-heavy pages with variable third-party content. This is not a defect in the API. It is what happens when you sample a stochastic process once.

Three practical responses:

  • Take a median of several runs. Three runs, keep the middle score. Five if the URL is important and noisy. Your quota can absorb it.
  • Compare against a trailing baseline, not against yesterday. Store a rolling seven-day or fourteen-day median and alert when the current run departs from it by more than the historical noise band you have measured for that URL.
  • Alert on metrics, not on the composite. A score dropping from 78 to 71 tells you nothing actionable. Total Blocking Time going from 210ms to 640ms tells you a script got heavier, and the details object of unused-javascript or third-party-summary will often name the culprit.

Field data is immune to this particular problem because it is already an aggregate of thousands of loads. It has the opposite trait: it is so smooth that a genuine regression takes days to become visible, and a genuine fix takes weeks to fully land. Use lab data for fast detection and field data for confirmation. Neither one alone is a monitoring strategy.

Designing a monitoring loop that does not lie to you

A workable daily job looks like this:

  1. Assemble the URL list. Do not hand-maintain it. Pull the top pages by impressions from Search Console, add your key conversion pages, and add one representative URL per template: home, category, product, article, pricing, checkout. Twenty well-chosen URLs beat two thousand random ones. If you are wiring that list programmatically, the Search Console API guide covers pulling the query and page data that should drive the selection.
  2. Queue with bounded concurrency. Five to ten workers, each handling one URL at a time, with retry and backoff around 429 and 500 responses.
  3. Request mobile explicitly. Add desktop only if desktop traffic is a meaningful share of your sessions.
  4. Extract, then store. Pull the category scores, the five performance metric numericValue fields, both CrUX blocks including the distribution buckets, the Lighthouse version, and the analysis timestamp. Keep the raw JSON compressed in object storage for a rolling window if you want the ability to go back and answer a question you did not anticipate.
  5. Record failures as data. A row with a null score and an error code is more useful than a missing row.
  6. Alert on deltas against a baseline, with a minimum absolute change so you are not paged for noise.

Store the Lighthouse version alongside every result. Google ships new Lighthouse versions with changed weights and new audits, and when your scores drop four points across every URL on the same day, the version field is what tells you in ten seconds that the scoring changed rather than your site.

At Skopx, this pattern is what Site Health does: Lighthouse scores through the PageSpeed Insights API, real-user Core Web Vitals from CrUX, Search Console performance, and an in-house on-page SEO audit that produces a 0-100 score with a fix list, all on a schedule rather than on demand. If you would rather assemble it yourself from parts, the same job can be built as a scheduled automation on the Skopx platform alongside whatever else your team runs, and Skopx starts at $5 per month for Solo and $16 per seat for Team.

What to store, and how to alert on it

Keep the storage schema narrow and boring. One row per URL, per strategy, per run, with these columns:

  • URL, strategy, fetched-at timestamp, Lighthouse version
  • Category scores: performance, accessibility, best practices, SEO
  • Metric values: LCP, TBT, CLS, FCP, Speed Index, and server response time
  • Field p75 values plus the three distribution proportions for LCP, INP and CLS
  • A flag for whether the field data was URL-level or origin-level fallback
  • An error code column, null on success

That schema answers most of the questions anyone will ask: whether a template regressed, whether a fix moved the field numbers, whether one page is dragging an otherwise healthy site, and whether the number you are quoting is real user data or a fallback.

For alerting, thresholds that survive contact with reality:

SignalTrigger worth alerting on
Performance scoreDrops 10 or more points below the 14-day median
Total Blocking TimeIncreases 50% or more and exceeds 300ms
Largest Contentful Paint (lab)Increases by 1 second or more versus baseline
Field CLSp75 crosses from good into needs-improvement
Field LCP poor bucketPoor proportion increases by 5 percentage points or more
Run failuresSame URL errors on two consecutive days

Every alert should carry the diff, not just the current value, and should link to the stored raw report so whoever picks it up can read the details arrays without re-running anything.

Finally, resist the urge to turn a Lighthouse score into a company-wide KPI. It is a proxy, weighted by Google's judgment about what matters, and it is measuring one synthetic load. The field data is the honest number, the lab data is the debugging tool, and the composite score is a convenient summary that hides both. If you want a single number for site quality, build it from several inputs rather than borrowing this one, an approach we work through in what an SEO health score really measures. And if the point of all this is a periodic review rather than continuous alerting, a structured pass through the website audit checklist will catch categories of problem that no performance API reports at all.

Frequently Asked Questions

Do I need an API key to use the PageSpeed Insights API?

Technically no, and practically yes. Unkeyed requests get a small anonymous allowance intended for casual testing, and they will start failing quickly under any automated load. Create a Google Cloud project, enable the endpoint in the API library, generate an API key, and restrict it by server IP address. The key is free. There is no billing tier attached to this endpoint, so the only ceiling is the quota.

Why is loadingExperience missing from my response?

The URL does not have enough Chrome UX Report traffic to produce an anonymized aggregate over the trailing 28-day window. This is normal for most pages on most sites, and it is not an error. Fall back to originLoadingExperience, which aggregates across the whole origin and is available far more often, and mark clearly in your storage and your dashboard that the number came from the origin rather than the page. Presenting origin data as page data is the most common way these dashboards mislead people.

Can I batch multiple URLs in one request?

No. The endpoint takes exactly one URL per call, and the older global batch HTTP endpoint that some tutorials still reference has been deprecated. Concurrency at the client is the answer: a worker pool of five to ten parallel requests keeps you well under the 240 queries per minute limit while getting through a few hundred URLs in a manageable window. Add exponential backoff on 429 responses so a burst does not cascade into a wave of retries.

How do I get the same score I see in the PageSpeed Insights web interface?

Match the parameters. The web interface runs mobile by default while the API defaults to desktop, which accounts for most reported mismatches. Beyond that, expect some variance regardless: two runs of the same URL with identical parameters minutes apart routinely differ by a few points because network conditions, origin response time and third-party scripts all vary. If you need a stable comparison, take the median of three or five runs rather than trying to reproduce a single number exactly.

How often should I run it?

Daily is right for most sites. Field data updates on a 28-day rolling basis, so polling CrUX more often than daily adds nothing. Lab data is noisy enough that hourly runs mostly generate false alarms. The exception is deployment: running your key templates immediately after a release catches the regression while the change is still fresh in everyone's mind, which is worth far more than another scheduled sample. Pair the daily baseline job with a post-deploy check and you have covered both.

Does a higher Lighthouse score improve rankings?

Not directly. Google's page experience signals are based on field Core Web Vitals from real users, not on the Lighthouse composite score, and they are one input among many rather than a primary ranking factor. A page can score 100 in the lab and still have poor real-user metrics if actual visitors are on slower devices and networks than the test profile assumes. Optimize the field numbers, use the lab score as the fast feedback loop that tells you whether a change moved anything, and keep expectations about ranking impact proportionate.

Share this article

Skopx Team

The Skopx engineering and product team

Related Articles

Stay Updated

Get the latest insights on AI-powered code intelligence delivered to your inbox.