SevyDevy

Guide · Frontend

Real User Monitoring: Measure What Users Actually Feel

27 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • Real user monitoring (RUM) measures how fast and smooth your site is for real visitors, on their own phones and networks, instead of on your test machine.
  • A small script in each page records LCP (when the main content appears), INP (how fast the page reacts to taps) and CLS (how much the layout jumps).
  • The script sends them in one beacon, a tiny background request, when the page is hidden. That covers switching apps, closing the tab or moving on.
  • Thousands of visits give a spread, not one number. So you read the 75th percentile (p75), the value that three in four visits are at or below, per page, device and network.
  • Beginners can stop after the section on p75. Engineers then get findings from running the code: CLS arrives only from Chromium browsers such as Chrome and Edge, and averaging p75 values was off by over three seconds. They also get a bug in single-page apps, sites that switch screens without loading a new page, that files numbers under the wrong page.

What goes wrong when you only test site speed on your own machine?

You ship a page that feels fast to you and slow to most of your visitors, and nothing on your dashboards says so. Your tests measure your laptop and your office Wi-Fi, not their phones and their networks.

Picture a common setup. An online store sells soaps, shampoos and face washes made in small batches. Most shoppers open it on low-cost Android phones over mobile data, many in small towns around Kanpur. The setup is made up, but the problem is real.

The frontend team checks every release with a lab tool, a program that loads the page in a controlled test and gives scores. The product page shows its main photo in about 1.6 seconds (made up). Every score is green.

The growth team sees a different picture. Visits from small towns are rising, but fewer of those visitors add anything to the cart. Some people want to blame the prices.

Then someone asks a simple question. How fast is the product page for the people actually buying soap on it? Nobody knows. The team only knows how fast it is for the team.

Real user monitoring answers that question. This guide follows the store as it adds real user monitoring: what the script measures, how the data travels, how to read it, and what to keep out of it.

What is real user monitoring?

Real user monitoring, or RUM, measures performance inside the browsers of your real visitors and collects those numbers for analysis. Each visit reports what that person actually experienced.

Performance here means how fast the page shows its main content, how quickly it reacts to taps, and how steady the layout stays. Data gathered this way is called field data, because it comes from the field, not the lab.

Think about buying a mattress. In the showroom you lie on it for five minutes, in a cool room, in your office clothes. It feels perfect. The real test is months of nights at home, in summer heat, with your own back.

A lab test is that showroom lie-down: one controlled try. RUM is asking every buyer how they slept, every night. Where the picture breaks: RUM collects timings, not opinions, and it only hears from visits that load far enough to run its script.

A RUM setup has three parts. A small script runs in every page. A collector on your server receives what the script sends. A dashboard turns millions of tiny reports into a few numbers people can act on.

What is the difference between real user monitoring and synthetic monitoring?

Synthetic monitoring runs scripted visits from machines you control, while real user monitoring measures visits from actual people. Synthetic tells you what can happen. RUM tells you what did happen.

Synthetic tests are also called lab tests. They load the page on one chosen device profile, network and location, usually with an empty cache, meaning no files saved from earlier visits. Results are steady from run to run, which makes them good for catching a slow build before it ships.

Field data differs from lab data for several reasons. Real visitors use a mix of devices, networks and places. Returning visitors may already have files cached. Real people also scroll and tap at moments a lab script cannot predict.

That last point matters most for INP, which measures how quickly the page responds to taps, clicks and key presses. A page-load lab test has no real taps, so lab tools report Total Blocking Time (TBT) instead. TBT is how long the main thread, where the browser runs your page's JavaScript and handles taps, was too busy to respond during loading. It only hints at INP.

Synthetic monitoring and RUM compared on the store's product page (numbers made up)
Synthetic (lab) testReal user monitoring
Who loads the pageA script on a machine you choseEvery real shopper who visits
Devices and networksOne profile per runHundreds of phone models and networks
Result shapeOne number per runA spread of thousands of numbers
Product page LCP1.6 seconds in the lab tool4.8 seconds at p75 on mobile
Measures INPOnly for taps you script; page-load tests show TBT insteadYes, from real taps
Works before launchYesNo, it needs real traffic
Best useBlock slow builds before releaseFind who suffers and how much

Keep both. The team keeps its lab check in the release pipeline as a gate. RUM becomes the scoreboard that tells the growth team whether a change helped real shoppers.

What does a RUM script measure in the browser?

A RUM script measures loading, responsiveness and visual stability, plus context about the visitor's device and network. The browser already exposes most of this through built-in performance interfaces.

The headline numbers are usually the Core Web Vitals, three user-focused metrics with published thresholds for a good experience.

  • LCP, Largest Contentful Paint: when the largest image, text block or video in the visible screen finishes rendering. Good is within 2.5 seconds of the start of loading.
  • INP, Interaction to Next Paint: how long the page takes to show a response after a tap, click or key press. It reports about the slowest one in the visit. Good is 200 milliseconds or less.
  • CLS, Cumulative Layout Shift: a score for how much the content jumps around unexpectedly. It has no unit. Good is 0.1 or less.

INP ignores scrolling and hovering. On pages with many taps, it skips one of the slowest for every 50 interactions, so one freak tap does not decide the score. A visit with no taps reports no INP at all.

A related guide on Core Web Vitals explains how to fix each one. Here we care about collecting them from every visit.

Under the hood, the browser follows web standards for timing. Navigation Timing Level 2 gives one entry per page load. It is a Working Draft (latest dated 1 September 2026) from the W3C, the group that publishes web standards. You read it with performance.getEntriesByType("navigation").

That entry holds timestamps such as responseStart, when the first byte of the page arrived. Its type field says whether the visit was a fresh navigation, a reload or a back or forward move.

Timings alone are not enough. The script also records context, the facts that let you group visits later.

  • The page path without the query string, the part of the address after a question mark. The backend later maps it to a page type, such as home, product or cart.
  • The browser's network estimate, such as 3g or 4g. It comes from recently measured speed and delay, not from the phone's actual network type.
  • Rough device memory in gigabytes, rounded to a power of two, which separates cheap phones from expensive ones.
  • The release version of the site, so a slow day can be tied to a deploy.
  • A random id per metric, so repeated reports of the same metric from one page view can be merged.

Not every browser reports every signal. Several signals come only from Chromium browsers, the family that includes Chrome and Edge. That matters later, because a slice can only contain visits that sent the field you slice by.

Which browsers can report each signal, from MDN browser compatibility data 8.1.6 (checked 19 October 2026)
SignalChrome and EdgeFirefoxSafari, including on iPhone
LCPYes, since Chrome 77Yes, since 122Yes, since 26.2
INPYes, since Chrome 96Yes, since 144Yes, since 26.2
CLS (layout shift entries)Yes, since Chrome 77NoNo
Network estimate (effectiveType)YesNoNo
Device memory (deviceMemory)Yes, HTTPS pages onlyNoNo

Why do RUM dashboards use the 75th percentile instead of the average?

RUM dashboards use the 75th percentile because field data is a spread, and an average hides the slow visits. The 75th percentile, or p75, is the value that three in four visits are at or below.

Back to the mattress. Three people in a family sleep well and one wakes with a sore back every morning. The average sounds fine. The person with the sore back still sleeps badly.

Averages also get pulled around by a few extreme values. One visit stuck on a dead connection for 60 seconds can drag the average far from what most people saw.

The Core Web Vitals guidance measures p75 of page loads, split into mobile and desktop. A page meets a threshold when at least three in four visits get a good experience.

Here are the store's first numbers for the product page (made up). Over one week, about 70,000 product page views report LCP. The median, or p50, is 2.9 seconds. The p75 is 4.8 seconds, and the p95 is 9.1 seconds.

The lab result of 1.6 seconds was real. It was simply one point, near the very fast end of the spread.

How do you send RUM data without slowing the page down?

You queue measurements in memory and send them in one small beacon when the page becomes hidden. A beacon is a tiny background request that the browser keeps trying to send even as the visitor leaves.

The browser API for this is navigator.sendBeacon. It queues a small POST request, one that carries data to a server, and returns at once. It returns false when the browser refuses to queue the data.

Timing matters. Older scripts sent data on the unload event, when the page closes. MDN says browsers often do not fire that event, especially on mobile. Chrome's guidance is to never use unload and to send on visibilitychange instead.

The visibilitychange event fires when the page becomes hidden. Hidden covers most ways a visit ends. The shopper switches apps, locks the phone, closes the tab or moves to another page.

Here is the store's collection script. It uses web-vitals 6.2.3 for the three metrics and adds a little context to each one.

typescript
import { onCLS, onINP, onLCP, type Metric } from "web-vitals";

const RELEASE = "web-412"; // set by your build, so a slow day can be tied to a release
const queue: object[] = [];
const nav = navigator as any; // connection and deviceMemory exist only in Chromium browsers

function addToQueue(m: Metric): void {
  queue.push({
    name: m.name, value: m.value, id: m.id, release: RELEASE,
    page: new URL(m.navigationURL ?? location.href).pathname, // path only, no query string
    net: nav.connection?.effectiveType ?? "unknown",
    mem: nav.deviceMemory ?? null,
  });
}

onLCP(addToQueue);
onINP(addToQueue);
onCLS(addToQueue);

addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden" && queue.length > 0) {
    navigator.sendBeacon("/rum", JSON.stringify(queue.splice(0)));
  }
});

The addToQueue function stores each metric with the page path and device hints, and no query string. The three on-functions ask the library to call addToQueue whenever a value is ready to report.

The page path comes from the metric's navigationURL, the address the page was loaded at, cut to its path. That keeps search terms in the query string out of the data. On this store, paths hold only product names. If your paths carry order or user ids, map them to a template such as /orders/ID before sending.

The listener at the bottom sends everything when the page hides. queue.splice(0) empties the queue as it reads it, so nothing goes twice. The path "/rum" is the store's own collector.

What we saw when we ran it

We ran this script in Chromium 141 against a local test server. We loaded a product page with ?q=secret-search in the address, tapped a button that blocks for 300 milliseconds, then left.

Exactly one beacon arrived, 413 bytes, holding LCP, INP and CLS. INP came out a little above the 300 ms the handler blocked: 304 to 376 milliseconds across our runs. The page field was "/product/neem-soap" with no query string. The body arrived as text/plain;charset=UTF-8, so the collector must parse text, not expect JSON headers.

Why was the tap's INP already in the queue? In web-vitals 6.2.3, the library's own visibilitychange listener runs in the capture phase, which comes before ordinary listeners like yours. So it adds the final INP and CLS before your listener sends the batch.

Metrics can arrive more than once

One detail surprises people. web-vitals calls you again for CLS and INP on a later hide, but only if the value changed. The README says the callback is always called on a hide; the 6.2.3 code skips it when nothing changed. Running the library's own reporting code showed one call at 120 ms, none on an unchanged hide, then 450 ms with a delta of 330.

After the back button restores a page from the browser's back/forward cache, every metric starts again with a new id. The library treats that restore as a separate visit.

That is why each report carries an id. The collector keeps the latest value per id, or adds up the delta field the library also sends.

How do you check the beacon on your own site?

Open Chrome DevTools on your page, go to the Network panel and type rum in the filter box. Switch to another tab and come back. A POST to /rum should appear with type ping, and its payload shows your JSON.

To see your own LCP for the current page, paste this into the DevTools console. It prints the time in milliseconds and the tag of the LCP element.

javascript
new PerformanceObserver((list) => {
  const last = list.getEntries().at(-1);
  console.log("LCP", Math.round(last.startTime), "ms", last.element?.tagName);
}).observe({ type: "largest-contentful-paint", buffered: true });

On our local test page it printed lines such as LCP 56 ms H1, meaning the heading was the largest thing painted. On a product page you would expect IMG for the main photo.

So send numbers, not whole entries. The library's README warns that serialising the full metric object will likely include more data than you need. If sendBeacon still returns false, that batch is lost, because splice already emptied the queue.

The diagram shows one shopper's visit from start to dashboard.

One product page visit, measured and reported

Measurements wait in memory and leave in one beacon when the page hides. Notice that nothing is sent while the shopper is browsing.

Diagram connections:

  • Shopper opens product page to Script queues LCP, INP, CLS
  • Script queues LCP, INP, CLS to Shopper switches apps
  • Shopper switches apps to Send one beacon (visibilitychange)
  • Send one beacon to Save the raw reports
  • Save the raw reports to Update p75 dashboard
  • Shopper opens product page: Phone → store website
  • Script queues LCP, INP, CLS: Browser → RUM script queue
  • Shopper switches apps: Page state → hidden
  • Send one beacon: Phone → RUM collector
  • Save the raw reports: Collector → event store
  • Update p75 dashboard: Event store → dashboard

How do you turn raw RUM reports into p75 per slice?

You keep the last report per metric id, group the values by slice, and read the 75th percentile of each group. A slice is one combination of context fields, such as product page on 3g.

Here is a simple version in TypeScript, the kind you might run for a daily report. It compiles under TypeScript 5.9.3 in strict mode and runs on Node.js 22.

typescript
type Beacon = { name: string; value: number; id: string; pageType: string; net: string };

// Nearest-rank percentile: the smallest value that at least p% of visits are at or below.
function percentile(values: number[], p: number): number {
  const sorted = [...values].sort((a, b) => a - b);
  return sorted[Math.ceil((p / 100) * sorted.length) - 1];
}

function p75BySlice(beacons: Beacon[], metric: string) {
  const latest = new Map<string, Beacon>();
  for (const r of beacons) if (r.name === metric) latest.set(r.id, r); // last report per id wins
  const groups = new Map<string, number[]>();
  for (const r of latest.values()) {
    const key = `${r.pageType} | ${r.net}`;
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key)!.push(r.value);
  }
  return [...groups].map(([slice, v]) => ({ slice, p75: percentile(v, 75), visits: v.length }));
}

// Two collectors with very different traffic (made-up LCP values in milliseconds).
const serverA = Array.from({ length: 900 }, (_, i) => 1500 + i);  // 1500 to 2399 ms
const serverB = Array.from({ length: 100 }, (_, i) => 7000 + i * 20); // 7000 to 8980 ms
const avgOfP75s = (percentile(serverA, 75) + percentile(serverB, 75)) / 2;
console.log("p75 A:", percentile(serverA, 75), "p75 B:", percentile(serverB, 75));
console.log("average of the two p75s:", avgOfP75s, "true p75 of all visits:", percentile([...serverA, ...serverB], 75));

The percentile function sorts the values and picks the one at the 75 percent mark, using the nearest-rank method. The latest map keeps only the last report for each id, which handles repeated reports.

The key joins page type and network into one slice name, such as "product | 3g". It also returns the visit count, so a dashboard can grey out tiny slices.

The last lines show a trap that appears in interviews. Two collectors each compute p75 for their own visits. Server A had 900 fast visits and server B had 100 slow ones (made up).

Pause and predict: what is the true p75 of all 1,000 visits? Is it near the average of the two p75 values?

text
p75 A: 2174 p75 B: 8480
average of the two p75s: 5327 true p75 of all visits: 2249

What just happened? Averaging the two p75 values gave 5,327 ms, but the true p75 of all visits is 2,249 ms. The average gave the 100 slow visits the same weight as the 900 fast ones.

Sorting every value works for a daily report. At large scale, teams keep histograms or sketches instead. These are compact summaries that estimate percentiles without keeping every value.

Choose a summary that can be merged. The DDSketch paper calls this mergeability: several combined sketches must be as accurate as one sketch of the same data.

How do you slice RUM data to find the users who suffer?

You slice RUM data by the context fields you collected, such as page type, device memory, network and region, and compare p75 across slices. The slow group usually stands out quickly.

The team breaks product page LCP down by network estimate and device memory (numbers made up). On 4g with 8 GB of reported memory, p75 is 2.4 seconds. On 3g with 2 GB or less, it is 7.6 seconds.

The growth team adds a region slice, worked out at the collector from the network address before that address is dropped. Most of the slow 3g visits come from small towns, the same visitors who stopped adding to the cart.

Next, the team joins RUM data with cart events using the page view id. Visits with LCP under 2.5 seconds add to the cart at about 9 percent. Visits over 6 seconds add to the cart at about 3 percent (made up).

That does not prove speed alone causes the drop. Slow visits may also come from shoppers with less money to spend. But it gives the team a reason to test speed before cutting prices.

The cause turns out to be simple. The product page loads a 1.4 MB hero photo of a soap bar, at full size, on every phone. On a slow connection, that one photo takes seconds.

  1. Find the worst slice

    Sort slices by p75 LCP and by visit count. The 3g, low-memory product page slice is both slow and large.

  2. Read the attribution

    The web-vitals attribution build adds a target field that names the LCP element. For this slice it is the hero photo, almost every time.

  3. Fix it

    Serve smaller, compressed versions of the photo for small screens, and tell the browser to fetch it early.

  4. Release to a share of users

    The fix goes to 10 percent of visitors first, tagged with a new release value in the RUM data.

  5. Compare the two groups

    After a week, p75 LCP for the 3g slice drops from 7.6 to 4.1 seconds on the new release (made up).

  6. Check the business number

    The growth team checks add-to-cart for the same slice. It rises, so the fix goes to everyone.

What can real user monitoring not see?

RUM only sees visits that run your script, in browsers that support the measurement, inside your own page. Each gap skews the numbers in a known direction, so name the gap on the dashboard.

Browsers that cannot report a signal

CLS comes only from Chromium browsers, so your CLS p75 says nothing about Safari or Firefox visitors. The network estimate and device memory are Chromium-only too, so every Safari and Firefox visit lands in an unknown slice.

That is why the script sends "unknown" and null instead of guessing. Count the unknown slice as its own row, and never fold it into a low-memory or slow-network bucket.

Visits that never report

RUM only hears from visits that load far enough to run the script and send a beacon. A shopper who gives up on a blank screen may never report anything.

So a very slow page can look better in RUM than it really is. One fix is a tiny inline snippet early in the page that sends a started beacon. Started visits with no metrics then show up as a count of abandoned loads.

Also, web-vitals sends no CLS, FCP or LCP for a page that was opened in a background tab. FCP, First Contentful Paint, is when anything first appears.

Content inside iframes

The web-vitals README says the underlying browser APIs cannot see inside iframes, frames that embed another page, not even same-origin ones. Public field data from the Chrome UX Report does count iframe content, so the two can disagree on pages built from embedded frames.

Public field data is a cross-check, not a replacement

The Chrome UX Report (CrUX) is Google's public field dataset. It only includes Chrome users who turned on usage statistics reporting and browsing-history sync without a sync passphrase.

Its methodology page lists Chrome on desktop and Android. It excludes Chrome on iOS, Android WebView apps and other Chromium browsers such as Edge. Sites must be publicly discoverable and popular enough to qualify.

How do you build a RUM pipeline on the backend?

A RUM backend has a collector that accepts beacons, a queue that absorbs bursts, workers that clean and aggregate the data, and storage for dashboards and alerts. It is a small analytics pipeline.

Look at the diagram for where the work happens. The collector stays thin and fast. The heavy work, such as merging repeat reports and computing percentiles, happens later in workers.

A time-series store keeps one summary per slice per time window, such as p75 per slice per 5 minutes.

The store's RUM pipeline

The collector only checks and queues. Workers merge repeat reports and do the percentile maths.

Diagram connections:

  • Shopper's browser to RUM collector (Beacon on page hide)
  • RUM collector to Event queue
  • Event queue to Aggregation worker
  • Aggregation worker to Raw event archive
  • Aggregation worker to Time-series store
  • Time-series store to Dashboards and alerts
  • Shopper's browser: Runs the RUM script, sends beacons
  • RUM collector: Checks size and fields, drops raw IPs
  • Event queue: Absorbs sale-day bursts
  • Aggregation worker: Merges reports, builds p75 per slice
  • Raw event archive: Kept 30 days for deep digging
  • Time-series store: p75 per slice per 5 minutes
  • Dashboards and alerts: What the frontend and growth teams read

Treat every beacon as untrusted

Anyone can send a request to your collector, including bots and broken scripts. So the collector rejects oversized bodies, unknown metric names and impossible values, like a negative LCP. It also caps how many beacons one source may send per minute.

Sample when traffic grows

Sampling means keeping only a share of visits, such as 1 in 10. The store is small enough to keep everything. A large store might sample to cut cost.

Decide sampling once per page view, in the script, before measuring. Then a sampled visit sends all its metrics, and an unsampled one sends nothing.

How do you set alerts on real user monitoring data?

You alert when the p75 of an important slice crosses a threshold for long enough, and only when that slice has enough visits. One slow minute should never wake anyone up.

RUM data is noisy by nature. A burst of shoppers on a weak network after a festive sale message can move the p75 for an hour without anything being broken.

The team keeps alerts few and sharp. The diagram shows the checks one alert passes through before it reaches a person.

When a RUM alert fires

Three checks stop noisy slices from paging anyone. The thresholds are the team's own (made up).

Diagram connections:

  • Compute p75 every 5 minutes to At least 300 visits?
  • At least 300 visits? to p75 over 4 seconds? (Yes)
  • At least 300 visits? to Keep watching (No)
  • p75 over 4 seconds? to Over for 30 minutes? (Yes)
  • p75 over 4 seconds? to Keep watching (No)
  • Over for 30 minutes? to Alert the frontend on-call (Yes)
  • Over for 30 minutes? to Keep watching (No)
  • Compute p75 every 5 minutes: Product page, mobile, LCP
  • At least 300 visits?: Small samples are too noisy
  • p75 over 4 seconds?: Above the normal range
  • Over for 30 minutes?: Short spikes are ignored
  • Alert the frontend on-call: Includes release and top slice
  • Keep watching: No action needed

Good alerts also carry context. The alert shows the release with the biggest change, so a bad deploy is obvious at a glance. Weekly trends go to a dashboard instead, where slow drifts are easier to see.

How does real user monitoring work in single-page apps?

In a single-page app, moving between screens usually changes the address without loading a new document. Without extra support, web-vitals reports one set of metrics per full page load, and INP and CLS keep counting across route changes.

That breaks naive labelling. An earlier version of this guide built the page field from location.pathname at the moment each metric was ready.

We ran that older script, then changed the route to /cart with history.pushState, the call a single-page app uses to change the address, before leaving. LCP was filed under the product page, but INP and CLS were filed under /cart, a page that had not been loaded.

The script above uses the metric's navigationURL instead. In the same test, all three metrics stayed on the product page.

So if you turn it on, slice by browser before comparing routes. Browsers built on Chromium 151 or later, such as Chrome and Edge 151, will report per route. Older versions and other browsers report once per load.

What privacy rules apply to real user monitoring?

RUM should collect performance numbers and coarse context only, never personal data such as names, phone numbers, addresses or full URLs with query strings. Whether you also need consent depends on the law that applies to you, so check with your privacy or legal team.

Performance data looks harmless, but it leaks easily. A query string might hold a search term, an order id or an email address. That is why the script sends only the path.

  • Strip query strings and any ids from paths before sending.
  • Work out a rough region at the collector, then drop or shorten the network address.
  • Never record what people type, only how quickly the page reacted.
  • Keep raw events for a short time, such as 30 days, and keep only aggregates longer.
  • Follow your consent rules, and let the RUM script respect a visitor's choice.

This is also good engineering. The less personal data the pipeline holds, the less there is to protect, and the fewer people need access.

How do you explain real user monitoring in a system design interview?

Explain it as a loop: measure in the browser, send cheaply, aggregate by slice, then act on the worst slice and check the result. Each step should name one trade-off.

  1. Start with the user problem

    Say lab tests run on controlled machines, so they miss what real shoppers on slow phones feel.

  2. Name the metrics

    LCP for loading, INP for responsiveness, CLS for stability, plus context like page, device and network.

  3. Describe collection

    A small script queues metrics and sends one sendBeacon request when the page becomes hidden, under the 64 KiB limit.

  4. Draw the backend

    Collector, queue, aggregation workers, a time-series store for p75 per slice, and a short-lived raw archive.

  5. Explain the maths

    Report p75, not averages. Merge raw values or mergeable sketches across servers, never average percentiles.

  6. Cover scale and trust

    Sample per page view, validate every beacon, rate-limit the collector, and alert only on large slices.

  7. Close with blind spots and privacy

    CLS is Chromium-only, abandoned loads may never report, and no personal data or query strings leave the page.

Interviewers like a concrete number. Say something like this: the lab shows 1.6 seconds, but RUM shows a p75 of 4.8 seconds on mobile, mostly on 3g. Then explain how you would find the cause.

Questions people ask

Is real user monitoring the same as web analytics?
No. Web analytics counts visits, clicks and sources of traffic. Real user monitoring measures how fast and smooth each visit felt, such as LCP and INP. Some analytics tools can carry performance numbers too, but the questions differ. RUM needs percentiles, visit counts and device slices to be useful.
Does adding a RUM script slow down my website?
It can, if it is built badly. The web-vitals library says it is about 3 KB compressed, and its README says to defer it until after code that users need. It sends a small beacon when the page hides. Measure your own site with and without it in a lab test to confirm the cost is tiny.
What is a good sample rate for real user monitoring?
It depends on your traffic and budget. Small sites can keep every visit. Larger sites often keep a fraction, as long as each slice they care about still gets a few hundred visits per alert window. Decide once per page view, so a sampled visit reports all of its metrics.
Why do my RUM numbers differ from my lab test results?
They measure different things. A lab test is one run on one device, network and location, usually with an empty cache. RUM covers every real visitor, with slower phones, weaker networks, warm caches and real taps. Expect a wide spread, and trust RUM for how users actually feel.
Why is my CLS based on fewer visits than my LCP?
Because only Chromium browsers report layout shifts. As of 19 October 2026, MDN's compatibility data shows LCP and INP in Chrome, Firefox and Safari, but CLS only in Chromium. So Safari and Firefox visits send LCP and INP but never CLS. Read each metric's visit count before comparing them.
How long should I keep RUM data?
Keep raw events briefly and aggregates for longer. Raw events help you dig into a recent problem, so a few weeks is common. Percentiles per slice per hour or day are small and safe to keep for months, which lets you see slow trends across many releases.
Do I still need RUM if I use the Chrome UX Report?
Usually, yes. The Chrome UX Report only covers eligible Chrome users on desktop and Android, not Chrome on iOS, and only public sites popular enough to qualify. It cannot be sliced by your release, page template or checkout step. Use it as a public cross-check, and your own RUM for daily decisions.

Sources and further reading

Practise this with Lala

Practise