
We ship a fix for a performance problem, verify it works with lab tools, and sit there staring at CrUX waiting for the number to drop. A week goes by and nothing moves. Another week, still the same. It feels like the data is arriving late, or like the change did nothing.

Neither is true (well, the second one I can't guarantee). In my audits this is one of the confusions I have to clear up most often, and it almost always comes from the same misunderstanding: conflating two things that are independent.

On one side there's **the size of the aggregation window**. On the other, **the publishing cadence**. The window is always 28 days. Daily, weekly and monthly aren't different windows: they're different cadences over the same data.

## Four doors into the same dataset

Every CrUX source aggregates the data over a 28-day rolling window. What changes between them is how often the data refreshes and how finely we can slice it.

| Source                 | Cadence                                                 | Window                                                              | Granularity                                                     |
| ---------------------- | ------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- |
| CrUX API               | Daily, around 04:00 UTC, roughly two days behind        | 28 rolling days                                                     | Origin or page · phone, tablet, desktop                         |
| History API            | Weekly, on Mondays, through the previous Saturday       | Weekly series; each point, 28 days. Up to 40 points (25 by default) | Origin or page · phone, tablet, desktop                         |
| BigQuery               | Monthly, the second Tuesday after the collection period | Last 28 days of the month                                           | Origin only · phone, tablet, desktop · approximated percentiles |
| PSI and Search Console | Derived from the API                                    | 28 days                                                             | Varies by tool                                                  |

The most commonly misread part: **the data isn't 28 days old, it's two days old**. What takes 28 days is for the window to fully turn over.

The History API row is the densest in the table, because it describes a time series rather than a single value. Here's the breakdown:

- **Each point is a _collection period_** whose value is computed over a 28-day window.
- **The points move week by week.** Each one spans 28 days but advances only 7, so they overlap with each other; we'll see this in detail below with CrUX Vis.
- **The API returns 25 points by default** and up to 40 if you ask for them with `collectionPeriodCount`, roughly 6 to 10 months of history.

## What happens to the window after a deploy

Each day that passes, the window drops one old day and picks up a new one. At first the fix sits in a tiny sliver; as the days go by, it takes up more of the window.

Drag the control to see how much of the window already holds post-fix sessions, and which percentile of the old data you're actually reading.

<figure>
  <iframe
    id="crux-win-demo"
    src="/demos/crux-28-day-window-en.html"
    width="100%"
    height="420"
    style="border: none; border-radius: 8px; display: block;"
    title="Interactive simulation of the CrUX 28-day rolling window after a deploy"
    loading="lazy"
  ></iframe>
</figure>
<script>
window.addEventListener('message', function (ev) {
  if (ev.data && typeof ev.data.cruxWinH === 'number') {
    var f = document.getElementById('crux-win-demo');
    if (f) f.style.height = (ev.data.cruxWinH + 8) + 'px';
  }
});
</script>

That third readout is what explains the feeling that nothing is happening. CrUX doesn't report the mean, it reports the **75th percentile**, and the p75 of a mixture doesn't move linearly with the fraction of new data.

## Why p75 looks stuck and then falls off a cliff

Assuming the fix puts every new session in the good bucket, at a fraction `f` of refreshed data the p75 of the mixture is equivalent to reading percentile `(0.75 − f) / (1 − f)` of the old distribution.

In practice:

- **f = 0.25**: you're reading p67 of the old data. A small shift, easy to mistake for noise.
- **f = 0.50**: you're reading p50. This is usually where the threshold gets crossed.
- **f = 0.75**: p75 becomes entirely determined by the new data.

So for the first week and a half nothing seems to move, then somewhere between week two and week three it drops all at once. The data isn't late: the metric is resistant to change by design. And if traffic isn't uniform, whether from weekends or regional seasonality, the curve distorts further, because CrUX weights by navigation volume, not by day.

## What you're actually looking at in CrUX Vis

[CrUX Vis](https://developer.chrome.com/docs/crux/vis) visualizes the History API, not the daily API. This has two concrete consequences:

- **Each point is a window, not a week.** There are 40 points, one per week, and each is labeled with the end date of its own 28-day window, always a Saturday.
- **Two consecutive points overlap by 21 days.** Only 7 days of each point are new. Three quarters of it was already in the previous point.

Drag the control to separate the two points and watch the overlap shrink. The dates are a real example from CrUX Vis: point A closes its window on Saturday, January 10, and point B is the one that comes one or more weeks later.

<figure>
  <iframe
    id="crux-overlap-demo"
    src="/demos/crux-vis-overlap-en.html"
    width="100%"
    height="440"
    style="border: none; border-radius: 8px; display: block;"
    title="Interactive simulation of the overlap between two points of the CrUX Vis series based on their separation in weeks"
    loading="lazy"
  ></iframe>
</figure>
<script>
window.addEventListener('message', function (ev) {
  if (ev.data && typeof ev.data.cruxOverlapH === 'number') {
    var f = document.getElementById('crux-overlap-demo');
    if (f) f.style.height = (ev.data.cruxOverlapH + 8) + 'px';
  }
});
</script>

That's why the series looks so smooth: each point is already a 28-day aggregate, and CrUX Vis draws them overlapping week by week. A jump between two adjacent points never represents a real weekly change, it represents the effect of swapping out 7 days of 28.

## CrUX is the scoreboard, not the feedback loop

The framing error is treating CrUX as a fast feedback system. Its slowness is a deliberate property: it smooths out noise so the number stays stable, because it's the field-data system of record behind the Core Web Vitals assessment. Fast feedback belongs in a different layer.

- **Synthetic · minutes.** DebugBear, Lighthouse CI. Per deploy and pre-production. Zero wait, but it's the lab.
- **Your own RUM · within hours.** Real field data with attribution: which element is the LCP, which target triggers the INP, which shift causes the CLS. CrUX will never give you that level of detail.
- **Daily CrUX API · confirmation.** Verifies that the fix is settling into the data that scores you. Query it daily, not once a month.

One operational note: if you need to slice by country, **BigQuery is the only CrUX source that exposes that dimension**. The daily API and the History API only give you form factor. For regional analysis, the monthly dataset isn't "the slow one", it's the only one that answers the question.

## Conclusion

CrUX doesn't measure our deploy. It measures the people who visit us over 28 days. If we want to know whether the fix worked today, we ask our RUM tool. If we want to know whether Google has registered it, we ask CrUX in three weeks.

References: [CrUX methodology](https://developer.chrome.com/docs/crux/methodology), [History API](https://developer.chrome.com/docs/crux/history-api) and [CrUX Vis](https://developer.chrome.com/docs/crux/vis) on Chrome for Developers.
