CrUX API in 2026: Query Core Web Vitals Field Data for Any URL or Origin

The CrUX API returns 28-day rolling p75 field data for LCP, INP, CLS, FCP, and TTFB on any origin or URL with enough Chrome traffic. Here's how to query it, read the histograms, and pair it with your RUM without getting lost in the discrepancies.

CrUX API 2026: Query Real Field Data

Updated: September 15, 2026

The CrUX API is Google's public HTTP endpoint that returns 28-day rolling Core Web Vitals field data (LCP, INP, CLS, FCP, and TTFB) for any origin or URL that receives enough real Chrome traffic to be aggregated. You POST a JSON body with an origin or url field plus an API key, and you get back p75 values, histogram bins, and the collection period. It's free, it's the same field data Google Search ranks with, and in 2026 the History API extends it to 40 weeks of weekly trend data. Honestly, I lean on it every Monday morning before I look at anything else.

  • The CrUX API returns p75 values plus full histogram bins for LCP, INP, CLS, FCP, and TTFB over a rolling 28-day window, updated daily.
  • Only origins and URLs with sufficient Chrome traffic appear. Low-traffic pages return a 404 NOT_FOUND, and iOS Chrome plus every non-Chrome browser is excluded.
  • Query by origin for the whole site, by url for a single page; segment by formFactor (PHONE, DESKTOP, TABLET) or effectiveConnectionType.
  • The CrUX History API (v1, weekly) gives you 40 weekly data points of the same metrics. That's how you spot regressions before Search Console flags them.
  • BigQuery hosts the full monthly historical dataset with RTT p75, released the second Tuesday of each month; FID was retired and RTT replaces the ECT dimension in 2026.
  • CrUX and your RUM will never match exactly (Chrome-only, opt-in filter, aggregated p75 versus your per-session beacons), so use both, not one.

What is the CrUX API and what does it actually measure?

The Chrome User Experience Report (CrUX) is a public dataset of real-world performance metrics collected from Chrome users who've opted in to browser statistics and history sync. The API, exposed at https://chromeuxreport.googleapis.com/v1/records:queryRecord, is the low-latency way to read that dataset for any origin or URL. Behind the scenes, Chrome ships performance events for pageviews that pass a set of eligibility filters: the page must be publicly indexable, the user must be signed in with sync on, telemetry must be enabled, and the origin must exceed a traffic floor Google doesn't publish exactly (roughly in the thousands of monthly visits, from what I've seen).

The metrics you get back are the same ones that feed Google Search's page experience signals: Largest Contentful Paint, Interaction to Next Paint (which replaced First Input Delay in March 2024, and FID data was removed from CrUX entirely in 2025), Cumulative Layout Shift, First Contentful Paint, and Time to First Byte.

Each metric is reported as a p75 value plus a three-bucket histogram (good, needs improvement, poor) with the cutoffs matching the Core Web Vitals thresholds. The single most important thing to internalize: every value is a 75th-percentile aggregate over a rolling 28-day window. Not an average. Not yesterday's data. Not this morning's deploy. If you're chasing a real-time signal, CrUX is the wrong tool. If you're chasing what Google Search sees, it's the only tool.

How do I get a CrUX API key?

Getting an API key is a five-minute job and doesn't require billing to be enabled on the Google Cloud project. Head to the Google Cloud Console, create or select a project, then search for "Chrome UX Report API" in the API Library. Click Enable. Under Credentials, click Create Credentials, then API key. Copy the key that appears. That's the whole flow. You can restrict the key to the CrUX API and to specific IP ranges under key restrictions, which I strongly recommend for any key you're going to embed in a monitoring script that runs on a fixed server.

Quotas are generous but not infinite. As of the 2026 quota tables, the CrUX API allows 150 queries per minute and 25,000 queries per day per project by default, which is enough to poll a few hundred URLs on a schedule without hitting the ceiling. If you need more, the History API has separate quotas and both can be raised on request.

Never put a raw API key into client-side JavaScript. The CrUX endpoint accepts the key as a URL query parameter, so anyone who inspects a network request would grab it. Route your queries through a server-side proxy or a scheduled job that writes the results into your own datastore.

Your first query: origin vs URL, form factors, and connection type

Every request is a POST with a JSON body. The absolute minimum body is {"origin": "https://example.com"} or {"url": "https://example.com/path"}. The difference matters. Origin-level data aggregates every page under that host, while URL-level data is scoped to a single page. Most sites have origin data for the whole host and URL data for their popular pages only.

So, here's a bare curl request I use to sanity-check a domain before I even wire up code:

# Origin-level query, all form factors combined
curl -s "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$CRUX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"origin": "https://web.dev"}' | jq '.record.metrics.largest_contentful_paint.percentiles'

To narrow the aggregation to mobile users, add a formFactor field with one of PHONE, DESKTOP, or TABLET. Omitting the field returns the combined aggregate across all three, which is what Search Console shows by default and rarely what you actually want when debugging. Mobile users on 4G typically drive the p75 higher than the combined figure suggests, so if your Search Console report says "needs improvement" for mobile, a form-factor-specific query is what confirms it.

# Mobile-only URL query with connection filter
curl -s "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$CRUX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://web.dev/vitals/",
    "formFactor": "PHONE",
    "effectiveConnectionType": "4G"
  }'

The effectiveConnectionType dimension accepts 4G, 3G, 2G, slow-2G, or offline, and lets you isolate slow-network experiences that a combined p75 hides. In 2026 Google added a new roundTripTime metric with its own p75 that you can pull without any filter. It's the honest per-user network baseline you couldn't previously get from CrUX. See the CrUX release notes for the full changelog, including the RTT rollout and the ECT dimension retirement in BigQuery.

Reading the response: histograms, p75, and collection periods

The JSON response has three top-level fields that matter: record.key (echoes what you queried), record.metrics (the numbers), and record.collectionPeriod (the start and end dates of the 28-day window). Each metric under record.metrics has two shapes: a histogram array with three bins each carrying a density between 0 and 1, and a percentiles.p75 value. For CLS, the p75 is a string like "0.08" because CLS uses two-decimal precision. Every other metric is an integer milliseconds value.

{
  "record": {
    "key": { "origin": "https://web.dev" },
    "metrics": {
      "largest_contentful_paint": {
        "histogram": [
          { "start": 0,    "end": 2500, "density": 0.82 },
          { "start": 2500, "end": 4000, "density": 0.13 },
          { "start": 4000,               "density": 0.05 }
        ],
        "percentiles": { "p75": 1980 }
      },
      "cumulative_layout_shift": {
        "histogram": [/* … */],
        "percentiles": { "p75": "0.03" }
      }
    },
    "collectionPeriod": {
      "firstDate": { "year": 2026, "month": 8, "day": 19 },
      "lastDate":  { "year": 2026, "month": 9, "day": 15 }
    }
  }
}

The histogram densities always sum to 1.0 (modulo float error). To compute the percentage of "good" experiences yourself, read histogram[0].density * 100. This is more useful than the p75 for storytelling. A p75 of 2400 ms tells you the current CWV threshold status, but "84% of your users see LCP under 2.5 s" is what wins the conversation with a product manager who's arguing that the redesign didn't hurt. Both come from the same response, so pull both, show both.

The CrUX History API: 40 weeks of trend data

The queryRecord endpoint gives you today's snapshot. The History API, at https://chromeuxreport.googleapis.com/v1/records:queryHistoryRecord, gives you the same shape of data but with parallel time-series arrays covering the last 40 weekly collection periods (roughly nine months). It updates once a week on Mondays. Every value in the series is still a 28-day rolling aggregate, so consecutive points overlap by three weeks; the series is not a set of independent samples. That's a feature for trend analysis and a footgun if you try to apply statistical tests that assume independence.

curl -s "https://chromeuxreport.googleapis.com/v1/records:queryHistoryRecord?key=$CRUX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"origin": "https://web.dev", "formFactor": "PHONE"}' \
| jq '.record.metrics.largest_contentful_paint.percentilesTimeseries.p75s'

The response replaces histogram with histogramTimeseries and percentiles with percentilesTimeseries. Each is an array whose i-th element corresponds to the i-th element of collectionPeriodTimeseries. Missing periods (for a URL that dropped below the traffic floor for a few weeks, say) show up as NaN for histogram densities and null for percentiles. Handle those before you plot them; a naive line chart will draw a spike through zero and lie to you. The official History API reference spells out the schema and the missing-data conventions in detail.

The practical use of history is regression detection. A weekly cron that pulls history for every landing page and diffs the newest p75 against a rolling median flags real problems roughly two weeks after they happen, which is faster than Search Console flags them and slower than your RUM. That's fine. CrUX history is your ground truth, and the delay is the cost of Google's own methodology.

CrUX on BigQuery: bulk historical analysis

For anything beyond nine months, or any analysis that needs full histograms across thousands of origins, use the CrUX BigQuery dataset. It's a public dataset published at chrome-ux-report.all and its country-specific siblings. New months land on the second Tuesday of the month for the prior month's data. Coverage in 2026 is roughly 15 million origins, with full metric histograms (not just p75) plus experimental metrics that don't ship to the API. RTT p75 landed in 2026 and ECT was retired from BigQuery around the same time because RTT gave a better signal at the same granularity.

-- Trailing p75 LCP by month for one origin, mobile only
SELECT
  yyyymm,
  SAFE_DIVIDE(
    SUM(CASE WHEN bin.start < 2500 THEN bin.density ELSE 0 END),
    1
  ) AS pct_lcp_good
FROM `chrome-ux-report.all.202601`,
     UNNEST(largest_contentful_paint.histogram.bin) AS bin
WHERE origin = 'https://web.dev'
  AND form_factor.name = 'phone'
GROUP BY yyyymm
ORDER BY yyyymm;

BigQuery is billed by bytes scanned; the CrUX tables are large, and unqualified queries can chew through your free tier fast. Always filter by origin in the WHERE clause and always pick a single monthly partition unless you actually need multiple. For SEO-adjacent work (competitor benchmarking, industry percentile curves, "how does the median news site render LCP") BigQuery is the only source that answers cleanly, and it's free within Google Cloud's monthly BigQuery allowance.

Why does my RUM data not match CrUX?

Because they measure different populations under different filters. This is the single most common question I field from stakeholders, and the answer they want ("someone is wrong") isn't the answer that's true. CrUX excludes every browser that isn't desktop or Android Chrome. iOS Chrome doesn't count, because it's actually WebKit under the hood. It excludes any Chrome user who hasn't opted in to sync-and-usage-stats. It excludes ineligible pages. It aggregates over 28 days at the p75. Your RUM, meanwhile, samples every user who lets the beacon fire, across every browser, on every device, and typically reports live or with a lag of minutes.

If your RUM library ships a p75 that's 400ms faster than CrUX's, the plausible reasons include: Safari users pulling your RUM p75 down while being invisible to CrUX; RUM sampling policies that drop the slowest-connection users disproportionately; a difference in what counts as a "pageview" (SPAs are especially prone to this); and RUM measuring the moment the beacon fires versus CrUX measuring what Chrome recorded before unload. Field data is field data; you just have two mildly different fields. The CrUX methodology documentation lists the exact eligibility filters, and I keep that page open when I explain the gap to leadership.

The right posture: use CrUX as the SEO ground truth for what Google will score you against, and use your RUM for debugging attribution and per-user breakdowns. If you don't have RUM yet, our guide on Real User Monitoring with the web-vitals library walks through the beacon setup that pairs with CrUX for a complete field-data picture.

Build a lightweight CrUX dashboard in Node.js

Here's a runnable script that pulls current p75 values for a list of URLs, dumps them to a CSV, and is the seed of every CrUX dashboard I've ever built. It uses only fetch (built into Node 20+), no dependencies, and it respects the rate limit with a 500 ms pause between requests. I hit this exact pattern shipping a nightly job for a client last spring, and it's held up without babysitting.

// crux-snapshot.mjs
// Usage: CRUX_KEY=xxx node crux-snapshot.mjs urls.txt > out.csv

import { readFile, writeFile } from 'node:fs/promises';
import { setTimeout as wait } from 'node:timers/promises';

const KEY = process.env.CRUX_KEY;
if (!KEY) throw new Error('Set CRUX_KEY');

const urls = (await readFile(process.argv[2], 'utf8'))
  .split('\n').map(s => s.trim()).filter(Boolean);

const rows = [['url', 'formFactor', 'lcp_p75', 'inp_p75', 'cls_p75', 'ttfb_p75', 'collection_end']];

for (const url of urls) {
  for (const ff of ['PHONE', 'DESKTOP']) {
    const res = await fetch(
      `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${KEY}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url, formFactor: ff }),
      },
    );

    if (res.status === 404) {
      rows.push([url, ff, '', '', '', '', 'not_enough_data']);
      continue;
    }
    if (!res.ok) {
      console.error(`[${res.status}] ${url} ${ff}`);
      continue;
    }

    const { record } = await res.json();
    const m = record.metrics;
    const cp = record.collectionPeriod.lastDate;
    rows.push([
      url,
      ff,
      m.largest_contentful_paint?.percentiles.p75 ?? '',
      m.interaction_to_next_paint?.percentiles.p75 ?? '',
      m.cumulative_layout_shift?.percentiles.p75 ?? '',
      m.experimental_time_to_first_byte?.percentiles.p75 ?? '',
      `${cp.year}-${String(cp.month).padStart(2, '0')}-${String(cp.day).padStart(2, '0')}`,
    ]);

    await wait(500); // stay under 150 rpm
  }
}

process.stdout.write(rows.map(r => r.join(',')).join('\n'));

Wire this into a scheduled job that runs once a day, append the output to a table, and you've got a poor-man's CrUX Vis for your own URL set. It's enough to spot the regressions that matter without paying for a third-party dashboard. Pair it with alerts on p75 breaches of the CWV thresholds and the performance budgets pattern from Lighthouse CI for pre-merge synthetic gates, and you've closed the loop between field and lab data.

Common gotchas: 404s, quota limits, and stale data

The two error responses you'll see most often are 404 and 429. A 404 with body {"error": {"code": 404, "message": "chrome ux report data not found", "status": "NOT_FOUND"}} means the URL or origin didn't cross the traffic floor for the queried window. It is not an error in the API sense, and any script that fails hard on 404 will fall over the first time you query a low-traffic path. Handle it as data-absent, not error. A 429 means you've blown the per-minute quota; back off with exponential retry and slow the loop.

Stale data is subtler. Because the window is 28 days and the p75 is the metric Google exposes, a hard drop in your actual field performance takes two to three weeks to fully propagate into the reported number. If you shipped a fix on the 1st and the p75 hasn't moved by the 8th, that's expected, not a bug. Cross-check with your RUM's daily p75 for the same URL on the same form factor. If that's already improved, you have proof the fix landed and CrUX will catch up. If your INP is the problem area, our deep dive on Interaction to Next Paint optimization covers the diagnostic playbook I run before I even wait for CrUX to confirm a fix.

One last gotcha: the API does not deduplicate URL fragments. https://example.com/page and https://example.com/page#top are different keys, and often only one of them has enough data. Always canonicalize URLs (strip fragments, normalize trailing slashes) before you query, or you'll get an inconsistent mix of 200s and 404s that isn't real signal.

Frequently Asked Questions

Is the CrUX API free to use?

Yes. The CrUX API and the CrUX History API are free with a default quota of 150 queries per minute and 25,000 queries per day per Google Cloud project. You need an API key, but Cloud billing does not need to be enabled. Quotas can be raised on request.

How often does CrUX API data update?

The queryRecord endpoint updates daily with a 28-day rolling window of the previous 28 full days. The History API updates weekly on Mondays with data through the previous Saturday. The BigQuery dataset updates monthly on the second Tuesday of each month for the prior month's data.

Does CrUX include Safari and Firefox users?

No. CrUX only records data from real Chrome users on desktop and Android who have opted in to sync and usage statistics. Chrome on iOS is excluded because it runs on WebKit rather than Blink. Safari, Firefox, and Edge users are not represented at all, so a RUM library is the only way to see them.

Why does my page return 404 from the CrUX API?

The URL or origin has not received enough eligible Chrome pageviews in the 28-day window to be aggregated. Google does not publish the exact traffic floor, but it's roughly in the low thousands per month. Query the origin instead of a specific URL, or fall back to your own RUM data for that page.

What is the difference between the CrUX API and BigQuery CrUX dataset?

The API returns a 28-day rolling snapshot (daily) or 40 weeks of weekly aggregates (History API) for one origin or URL at a time. BigQuery hosts the full monthly historical dataset across every eligible origin, with full histograms and experimental metrics. Use the API for automated monitoring; use BigQuery for bulk analysis or competitor benchmarking.

Nadia El-Sayed
About the Author Nadia El-Sayed

Core Web Vitals specialist focused on real-user monitoring. Believes synthetic-only perf testing is a comforting lie.