Using 75th Percentile for Real-World INP Targets
Interaction to Next Paint is the metric teams most often gate on the wrong statistic: a median INP looks healthy while the slowest quarter of taps feel sluggish, and a P99 fails the build on background-tab outliers nobody felt. The 75th percentile is the sweet spot Core Web Vitals itself uses, and this guide — part of the Percentile-Based Threshold Tuning reference — shows exactly why P75 is the standard and how to set, compute, and enforce a real-world P75 INP budget by device class.
Core Web Vitals scores a URL at the 75th percentile of field sessions, segmented by device, because it captures the upper-quartile experience without letting the worst 1–2% of sessions — driven by background-tab suspension, transient network stalls, or aggressive thermal throttling — dictate the verdict. A P75 INP at or below 200 ms on mid-range mobile over Fast 3G earns the "Good" tier; the CI fail line sits a buffer above the target to absorb measurement variance and synthetic-to-field drift.
Why the 75th Percentile Wins
To see why P75 is the anchor, look at the shape of a real INP distribution. Interaction latency is right-skewed: most taps resolve quickly, but an extended tail of slow interactions stretches far to the right. The median sits deep in the fast bulk and reports a rosy number even when a quarter of your users are waiting; P90 and above chase the volatile tail and swing wildly day to day on thin samples. P75 sits on the shoulder of the curve — high enough to represent the frustrated minority, low enough to stay stable.
This is not merely convention. Because P75 is defined off the sorted rank rather than the mean, one pathological 8-second interaction cannot drag it upward the way it would drag an average. That robustness is what lets you gate a build on it without chasing phantom regressions every time a tester's laptop thermal-throttles mid-run.
P75 INP Targets by Device Class
INP is dominated by main-thread availability, so the realistic P75 target diverges sharply by device tier. Gate each tier against its own P75; never pool device classes into one distribution. If you also gate INP on soft navigations inside a single-page app, treat each route transition as its own interaction population — see Budgeting Soft-Navigation INP in SPAs for that split.
| Device class | Connection | Field target (P75) | CI fail line | Min sessions / window |
|---|---|---|---|---|
| Desktop | Cable / Fiber | ≤ 150 ms | 180 ms | 1,000 / 7-day |
| High-end mobile | 4G / LTE | ≤ 200 ms | 240 ms | 1,000 / 7-day |
| Mid-range mobile | Fast 3G | ≤ 200 ms | 260 ms | 500 / 7-day |
| Low-end Android | Slow 4G | ≤ 300 ms | 350 ms | 500 / 14-day |
The 200 ms target is the "Good" ceiling Core Web Vitals applies at P75 regardless of device, but the fail line widens on slower tiers because their distributions are wider and noisier — a tight fail line on low-end Android over Slow 4G only generates false failures. Desktop over Cable earns a stricter 150 ms target because a fast CPU and stable connection leave no excuse for a slow next paint.
Diagnostic Steps
Confirm you have enough field data and a real P75 before you gate on it.
-
Check sample density. A P75 from too few sessions is unstable. Count sessions in the window per device class.
curl -s "$RUM_API/inp?device=high-end-mobile&window=7d" \ | jq '.sessions | length'Expected: a number at or above the floor in the table (≥ 1,000 here). Below it, widen the window to 14 days before trusting the percentile.
-
Compute the field P75. Pull the raw INP durations and interpolate the 75th percentile.
curl -s "$RUM_API/inp?device=high-end-mobile&window=7d" \ | jq '[.sessions[].inp] | sort_by(.) | .[(length*0.75)|floor]'Expected output: a value in milliseconds, e.g.
192— the P75 INP for high-end mobile on 4G over the window. -
Check the confidence bound. If the P75 swings more than ±15 ms between consecutive days, extend the window to 14 days or stratify by connection type before setting the budget. When samples are genuinely thin, see Interpolating Percentiles From Sparse Samples for a method that reports a P75 with an honest uncertainty band instead of a false-precision point value.
How Percentile Interpolation Works
The one-liner above uses floor, which snaps to the nearest lower sample. That is fine at scale but coarse on small windows, so the production evaluator below uses linear interpolation between the two samples that bracket the fractional rank. The rank is 0.75 × (n − 1); if that lands between indices, you blend the neighbours by the fractional part.
Interpolation matters most on the thin per-device windows where you have a few hundred sessions rather than tens of thousands. On a fat sample the two methods agree to within a millisecond; on a sparse one, snapping to a raw sample can jump the reported P75 by 20–30 ms as a single session enters or leaves the window.
Implementation
Compute the P75 from filtered field beacons and emit a single budget verdict. Discard sub-50 ms input noise and over-5,000 ms idle/background events, which otherwise distort the tail.
// scripts/p75-inp-budget.js
const TARGET = { "high-end-mobile": 200, "desktop": 150, "low-end-android": 300 };
const FAIL_LINE = { "high-end-mobile": 240, "desktop": 180, "low-end-android": 350 };
function p75(values) {
const v = values.filter((d) => d > 50 && d < 5000).sort((a, b) => a - b);
if (v.length === 0) return null;
const rank = 0.75 * (v.length - 1);
const lo = Math.floor(rank), hi = Math.ceil(rank);
return lo === hi ? v[lo] : v[lo] + (rank - lo) * (v[hi] - v[lo]);
}
function evaluate(device, durations, minSamples = 500) {
if (durations.length < minSamples) return { status: "WARN", reason: "low-sample" };
const value = Math.round(p75(durations));
const status = value > FAIL_LINE[device] ? "FAIL"
: value > TARGET[device] ? "WARN" : "PASS";
return { device, value, target: TARGET[device], fail: FAIL_LINE[device], status };
}
module.exports = { p75, evaluate };
The filter step is load-bearing. Sub-50 ms events are almost always synthetic or programmatic dispatches that no human perceived, and events beyond 5,000 ms are typically a tab that was suspended mid-interaction and resumed much later. Leaving either in the array shifts the sorted ranks and can move the P75 by tens of milliseconds. Field beacons feed this evaluator from your RUM stream; for the collection side — the PerformanceObserver wiring and payload design — see Injecting Custom Metrics via PerformanceObserver, and for the aggregation that turns raw beacons into a rolling P75 see Building P75/P99 Aggregation Pipelines.
CI Gating Assertion
Synthetic INP in CI complements the field gate. Capture it with the interaction feature enabled and fail the build when the lab P75 crosses the device fail line.
# .github/workflows/inp-p75-gate.yml
name: INP P75 Gate
on:
pull_request:
branches: [main]
jobs:
inp-gate:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20", cache: "npm" }
- run: npm ci && npm run build
- name: Collect with INP enabled
run: npx lhci collect --numberOfRuns=5
--collect.settings.chromeFlags="--enable-features=InteractionToNextPaint"
--url=http://localhost:8080/checkout
- name: Assert P75 INP
run: node ./scripts/p75-inp-budget.js --reports .lighthouseci --device high-end-mobile
The evaluator returns one of three verdicts, and the mapping from a measured P75 to a build outcome is deterministic: below target passes, between target and fail line warns, above the fail line blocks the merge. Keeping the WARN band explicit means a page that has drifted from 190 ms toward 235 ms surfaces as a warning annotation before it ever turns red, giving the owning team a lap to react.
Reconciling Lab and Field P75
Lab and field P75 measure related but distinct things, and expecting them to match exactly is a common trap. The CI job runs a scripted interaction on a throttled runner with no real user variance; the field number aggregates thousands of humans on wildly different hardware. A healthy relationship is that the lab P75 tracks the field P75 within roughly 10–15% and moves in the same direction when you ship a change. If the lab number is consistently far below the field number, your emulation is too gentle — revisit CPU throttling using the guidance in Calibrating CPU Throttling for CI Runners.
The point of the lab gate is not to reproduce the field number to the millisecond; it is to catch a regression before it reaches production. Set the lab fail line so that a change large enough to move the field P75 out of the "Good" band trips the lab gate first. When the two diverge structurally — for example, a slow interaction only real users on old devices hit — that is a signal to widen your device emulation matrix, not to loosen the gate.
Verification
After merging, confirm the gate measures what you intend:
- The job log prints a line such as
{ device: 'high-end-mobile', value: 192, target: 200, fail: 240, status: 'PASS' }— a numeric P75 below the target, notWARN: low-sample. - A deliberate slow-down (add a synthetic 250 ms blocking task to a click handler on a branch) flips the status to
FAILand exits non-zero. If it does not, the interaction feature flag is missing or events are being filtered out. - Field P75 in your dashboard tracks within roughly 10–15% of the lab P75; a persistent larger gap means the lab fail line is set too loosely relative to the field target.
Frequently Asked Questions
Why does Core Web Vitals use P75 for INP and not P90 or the median?
P75 covers three-quarters of real sessions, so a passing URL means most users have a good experience, while still ignoring the worst 1–2% of outliers caused by background-tab suspension or transient stalls. The median would hide a degraded slowest quarter; P90 and above become unstable in field data and over-penalise rare events. P75 is the balance point, which is why Google scores on it.
Should the CI fail line equal the 200 ms target?
No. Set the fail line a buffer above the target — roughly 240 ms on high-end mobile over 4G — so normal measurement variance and the synthetic-to-field gap do not flap the build. The 200 ms target is the field goal you steer toward; the fail line is the hard gate that blocks merges.
What if I don't have enough field sessions for a stable P75?
Widen the aggregation window from 7 to 14 days or stratify by connection type rather than gating on a thin sample. Until the session count clears the per-device floor, downgrade the assertion to a warning so an under-sampled P75 never blocks a merge. For a principled small-sample estimate, see the interpolating-percentiles guide linked above.
Why filter out sub-50 ms and over-5,000 ms interactions before computing P75?
Events under 50 ms are usually programmatic or synthetic dispatches no human perceived, and events over 5,000 ms are typically a tab suspended mid-interaction and resumed much later. Both sit at the extremes of the sorted array and pull the P75 rank away from the experience real users actually had, so removing them gives a truer number.
Should lab P75 in CI match field P75 exactly?
No — they measure different populations. A scripted run on a throttled runner should track the field P75 within roughly 10–15% and move in the same direction on a change. A persistent large gap means your CPU throttling is too gentle or your device matrix is too narrow, not that the gate is wrong.