Real User Monitoring
Field measurement from the machines your users actually own — how it differs from synthetic testing, why the two disagree, how to sample, and why watching the median hides the problem.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
My lab run is fast and support says the app is slow — which of the two is telling the truth about what people experience?
A person on a mid-range phone on a train opens your product. They are not a test case, they are the population. What they experience is the only definition of how fast the product is.
We run a performance test on every deploy in CI. It reports consistent numbers and it fails the build when they regress, so we know how fast the site is.
The test runs on a fixed machine, over a fixed connection, with a warm cache, in one browser, in one country, on one page, signed out, with no extensions installed. Your users match none of those on any axis.
- The test runs on a fixed machine, over a fixed connection, with a warm cache, in one browser, in one country, on one page, signed out, with no extensions installed. Your users match none of those on any axis.
- It cannot see the accounts with fifty thousand rows, the tab that has been open since Tuesday, the corporate proxy, the ad blocker that removed a script your code depends on, or the phone that thermally throttles halfway through hydration.
- It reports one number where the field produces a distribution. A distribution with a long tail can have a comfortable middle and a substantial minority of sessions that are unusable, and a single lab figure cannot represent that at all.
- It passes on every deploy while the experience degrades, because the degradation is device-dependent and the lab device is not degrading.
- The reverse also happens: the lab regresses on a metric that no user is affected by, the build goes red, and the team learns to override the gate.
What is actually happening
In the browser, not in the framework.
- Synthetic measurement runs your application in an environment *you* control, on a schedule you choose. Its defining property is repeatability: two runs differ only by the change you made, which is exactly what a regression gate needs.
- Real User Monitoring instruments the running application and reports from the browsers of actual users. Its defining property is representativeness: it samples the true joint distribution of device, network, browser, geography, account size and page state, which no lab can enumerate.
- RUM is built from platform APIs the page reads about itself — the Navigation Timing and Resource Timing entries,
PerformanceObserverentries for paint, layout and input, plus your own marks — and posts a summary before the page goes away (Vitals in the Field). - The transport matters. A page can be closed at any moment, so field beacons use
navigator.sendBeaconorfetch(..., { keepalive: true })from avisibilitychange/pagehidehandler rather thanunload, which is unreliable by design and skipped entirely on discard. - Sampling decides which sessions report. Head sampling picks at session start and is cheap but blind; tail-biased retention keeps everything in memory and decides at the end, so a slow or failing session is always kept (Sampling Without Throwing Away the Evidence in Observability & Performance).
- The output is a distribution, and distributions are summarised by percentiles, not by an average. The average is dominated by the bulk of the population; the tail is where the people who churn live (Percentiles: Which One, and How Many Users Is That? in Observability & Performance).
What this makes the browser do
And which of it is avoidable.
- Reading performance entries is cheap; the buffer is already populated by the browser whether you look or not. Do not build your own timers for things the platform already recorded.
- The beacon itself is one small request at page hide, on a path the user is no longer waiting for. That is the whole runtime budget a well-built RUM client should need.
- What is not cheap is the library. A field-monitoring bundle that ships hundreds of kilobytes and executes on the critical path is measuring a page it made slower — the observer effect, in a form the metric will not show you.
- Continuous sampling of anything — polling
performance.now()in a loop, snapshotting the DOM, tracking every pointer move — puts your telemetry into direct competition with the interaction it is trying to measure, on the one thread that renders (What the Main Thread Owns).
Two instruments, two different questions
The mistake is treating these as competing measurements of one quantity. They are measurements of different quantities, and the fact that they disagree is not a defect in either — it is the information.
A synthetic run holds everything constant except your code, which is the only way to attribute a change to a deploy. Field data holds nothing constant, which is the only way to learn what the product is actually like. Ask "did this change make it worse" of the lab; ask "what should we work on" of the field.
| Aspect | Synthetic / lab | RUM / field | Where they disagree, and why |
|---|---|---|---|
| Environment | One machine, one network, one browser, chosen by you | The true joint distribution of everything you did not choose | The lab device is usually near the top of the population; field CPU work is routinely several times heavier |
| Repeatability | High — two runs differ by your change | None — every session is a different world | The lab can gate a build; the field cannot, because it never holds anything still |
| Output shape | A number, or a small set of them | A distribution with a long right tail | A lab number lands somewhere in the field distribution, but nowhere predictable in it |
| Coverage | The journeys someone wrote a script for | Every journey a real person took, including the ones you did not design | The field finds the route nobody tests; the lab finds the regression nobody would have noticed yet |
| State | Empty account, cold or warm cache, signed out | Real accounts, real data volumes, real cache states, tabs open for days | Client work that scales with account data is invisible in the lab and dominant in the field |
| Latency to signal | Before merge | After the release reaches users | The lab is a gate; the field is a verdict — you cannot swap the roles |
| Third parties | Often blocked or stubbed for stability | Present, occasionally broken, sometimes blocked by the user | A vendor script degrading only shows up in the field, and looks like your regression |
Sampling: what you keep, and what you can no longer say
You will not keep every session, and you should not want to: full-fidelity collection costs bandwidth for the user, ingest and storage for you, and it maximises exactly the privacy surface you are obliged to minimise. Sampling is the decision about which sessions become evidence.
The important property is that different sampling strategies destroy different information. Uniform head sampling preserves the shape of the distribution and loses rare events. Tail-biased retention preserves rare events and destroys your ability to compute an unbiased rate — unless you record the sampling decision alongside the data, which is the step that is almost always skipped.
Which sessions report, and when is the decision made?
when Low-traffic internal tools, or a short deliberate investigation window on one route
cost Maximum ingest volume and maximum privacy surface; on high-traffic properties the bill and the data-protection review both arrive quickly.
when You want unbiased percentiles for common routes and can afford to miss rare failures
cost Rare and severe sessions are dropped at exactly the rate they are rare, so the worst experiences are the least likely to be in your data.
when You care about the sessions that went wrong — slow, errored, or beyond that route's usual range
cost Requires buffering the whole session in memory before deciding, and rates computed from the result are biased unless the sampling decision is recorded and corrected for.
when A small but important population — a device class, a region, an enterprise tenant — would otherwise be statistically invisible
cost More machinery, and every aggregate must be re-weighted by stratum or it silently over-represents the boosted segment.
when You need coverage and volume but not individual sessions
cost You cannot ever go back and ask a different question of the raw data; the drill-down from "the tail is bad" to "here is a session in the tail" no longer exists.
Why the median is the wrong thing to watch
Latency distributions in the field are right-skewed: a dense middle and a long tail of sessions many times slower. The median is a specific claim about the person exactly in the middle of that shape, and it is remarkably insensitive to the tail getting worse. You can double the pain of the slowest quarter of your users without moving it by a perceptible amount.
That is not an argument for watching only the tail either. A high percentile on a low-traffic route is dominated by a handful of sessions and jumps around for reasons that have nothing to do with your code. The workable answer is to watch both, per segment, with the sample count visible — and to remember that every one of those percentiles is a person, not a bar.
Page load (median, all users, all routes): steady Status: green — nothing to investigate —
Route /orders/:id, release 2026.08.3 device class p50 p75 p95 sessions desktop fast fast fast 18,402 mobile-high fast fast slower 9,155 mobile-low slower slow slow 2,806 <- 10% of traffic same route, previous release, mobile-low: unchanged same device class, other routes: unchanged => not a regression: a segment that was always this slow, and that the median has been hiding since launch.
The second view can be acted on because it names a population, a route and a release. It also happens to say the change is not a regression — which is a real answer, and one the single-number view can produce neither of. Aggregating across segments does not summarise the experience; it averages a working product and a broken one and reports the mean.
How to build it
Most important first.
- Run both, and use each for what it is for. Synthetic gates the deploy because it is repeatable; field tells you what to work on because it is real. Neither substitutes for the other, and the interesting engineering conversations start where they disagree.
- Report a distribution, not a number, and watch a high percentile alongside the middle. The middle tells you whether the typical experience moved; the tail tells you whether you have a segment that cannot use the product at all (Tail Latency: Why p50 Being Fine Does Not Help in Observability & Performance).
- Segment before you aggregate. Device class, connection class, country, route, release and signed-in state each split the population into groups whose experiences are genuinely different; a single global number averages a fast segment and a broken one into a healthy-looking figure.
- Always report the sample count next to the statistic. A high percentile computed from a handful of sessions on a rare route is noise wearing a number's clothes.
- Bias retention toward the interesting sessions: keep everything that errored, everything that exceeded a route's own historical distribution, and a uniform sample of the rest.
- Send once, late, and small. One beacon at page hide with a compact payload beats a stream of events, and survives the tab being closed.
- Instrument the interactions that matter to the product, not just page load. "Time from clicking Save to the row appearing" is a product metric; a page-level loading number cannot answer it (Interaction Responsiveness).
- Keep the RUM client tiny and load it so it cannot delay anything. If it needs a framework, it is the wrong client.
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Field data tells you how long things took. It cannot tell you whether the experience was operable — whether focus went somewhere sensible, whether the error was announced, whether the control could be reached without a pointer. There is no percentile for "a screen-reader user could complete the task".
- That asymmetry is the point: accessibility cannot be monitored the way latency can, so it must be verified rather than watched — in review, in automated checks, and in testing with real assistive technology (Accessibility Testing).
- Some field signals do carry accessibility information if you segment for it: input modality (keyboard versus pointer), the
prefers-reduced-motionandprefers-contrastpreferences, and text-zoom level all describe how people are using the product. Collect them as segmentation, and treat them as sensitive, because they can be health-adjacent (Contrast, Colour and Motion). - The monitoring client must not degrade what it observes. Work scheduled on the main thread to gather telemetry delays input handling and announcements alike, and the delay lands hardest on the slow devices you were trying to learn about (Long Tasks).
What can go wrong
- Survivorship bias: the beacon fires at page hide, so a session where the browser tab crashed, ran out of memory, or was closed in frustration before anything rendered is systematically missing. Your field data is skewed toward sessions that worked.
- Blocked ingest hosts remove a non-random slice of the population — privacy-conscious and heavily-extended browsers — which correlates with exactly the environments where things break.
- The sample changes when the product does. A marketing campaign that brings in a slower device population moves every metric without a single line of code changing (Release Health).
- Comparing across browsers as though the numbers were equivalent, when the underlying observers are not implemented uniformly (Vitals in the Field).
- Alerting on a metric with too few samples produces pages at three in the morning caused by one person on a bad connection (Alert Fatigue: The Page Nobody Reads in Observability & Performance).
- The monitoring library becomes a performance problem, and because it initialises before it measures, its own cost is partly invisible in its own numbers.
- Beacons arrive out of order and long after the fact. A report from a session that started before a deploy can land well after it, so ingest time and event time must be separate fields.
- A session that spans a deploy carries the old release identifier for its whole life. Attributing it to the release live at ingest time is a category error (Release Health).
- Clock skew between client and server is real and sometimes large. Durations measured with
performance.now()are monotonic and safe; wall-clock timestamps from the client are not. - The page can be hidden, restored from the back/forward cache and hidden again. A naive "report once at hide" fires more than once, and a naive "report only the first time" loses the rest of the session.
- RUM is population-scale behavioural data collection. The obligation attaches at collection, not at analysis: consent, retention limits and minimisation apply to a performance beacon exactly as they do to an analytics event (Analytics Events That Answer a Question).
- The default payload leaks more than teams expect. Full URLs carry ids and tokens; resource timing entries enumerate every third-party host the page touched; a raw user-agent string plus a viewport and a timezone is a serviceable fingerprint.
- Report route patterns, class the device rather than transmitting the full user-agent, and truncate or hash anything free-form before it leaves the page.
- A session identifier for grouping a single visit is not the same thing as a stable cross-visit user identifier. The second one is a substantially larger commitment, and it is very easy to create by accident by persisting the first (Persistent Client State).
- Resource timing exposes cross-origin request timings only in coarse form unless the third party sends
Timing-Allow-Origin. That restriction exists because precise cross-origin timings are a side channel, and it is a reason your third-party visibility is legitimately limited.
- "RUM replaces synthetic." It cannot gate a deploy: the field only tells you about a release after you have shipped it to people, and it cannot hold anything else constant.
- "Synthetic replaces RUM." It measures one machine you chose. It has never once told you what a user experienced.
- "The median looks fine, so we are fine." The median is the person in the middle. If a quarter of sessions are twice as slow, the median does not move at all, and those sessions are your churn.
- "The average is close enough to the median." For a right-skewed latency distribution the average sits above the median and below the tail, describing nobody in particular (The Average Was Fine and Users Were Not in Observability & Performance).
- "Our field numbers got worse, so we regressed." They also get worse when your traffic mix changes. Compare like segments before concluding anything about your code.
- "We have RUM, so we know what users experience." You know what the sessions that survived to page hide, on browsers that implement the observers, with the ingest host unblocked, chose to tell you.
Measuring it, and what changes in the field
- Percentiles per segment, with sample counts, per route and per release — the standard shape of a field dashboard (Percentiles: Which One, and How Many Users Is That? in Observability & Performance).
- The distribution's shape, not only its summary. A histogram shows a bimodal population — two device classes, or a cached and an uncached path — that percentiles alone will not reveal.
- Field against synthetic on the same journey, plotted together. The gap is a measurement of how unrepresentative your lab is, and it is worth tracking deliberately.
- Beacon delivery rate: beacons received against sessions started. A falling ratio means your field data is getting less representative, and no metric derived from it can be trusted while it does.
- The Performance panel and a throttled local profile for the mechanism, once the field has told you which journey to open (Measure Before Optimising).
- On a slow device the gap between lab and field widens most, because CPU-bound work — parse, compile, hydrate, style, layout — scales with the device while network time does not.
- On a high-latency or lossy network, variance dominates and the tail stretches far more than the middle moves, which is precisely the case a single lab run cannot represent (Packet Loss: Duplicate ACKs, Fast Retransmit and the RTO in Networking).
- With a large dataset in the account, client-side work grows with the data. A field metric segmented by account size often separates cleanly into two populations (List Virtualization).
- In a long-lived tab, the session accumulates state and the later interactions are slower than the early ones. A per-session summary hides that; per-interaction reporting does not (Memory Leaks).
- Field data is representative and noisy; lab data is precise and unrepresentative. You cannot buy both properties with one system, which is why running both is the answer rather than a compromise.
- Higher sampling rates give tighter confidence on rare routes and cost bandwidth, ingest volume, storage and a larger privacy surface. Sampling is a privacy control as much as a cost control.
- Segmenting deeply makes each number actionable and makes every group smaller, which is how a dashboard ends up full of statistics computed from too few sessions.
- Any client-side instrumentation you add costs the user something. The budget for observing an interaction has to come out of the interaction.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALNavigation Timing, Resource Timing,
performance.now()andnavigator.sendBeaconare supported across Chromium, Gecko and WebKit, so the basic field-collection loop — read the entries, summarise, beacon at page hide — works everywhere. What varies is which higher-level observers exist on top of it. - BROWSER-SPECIFICThe richer performance observers are not uniformly implemented: Chromium exposes the widest set including interaction and layout-instability entries, Gecko implements the loading and paint entries but not layout instability, and WebKit implements paint and resource timing without the event-timing surface — so a field dataset pooled across browsers is a mix of different measurements, not one measurement from a bigger sample.
- NETWORK-SPECIFICEffective connection type and downlink estimates come from the Network Information API, which Chromium exposes on Android and desktop while Gecko and WebKit do not expose it at all, so connection-class segmentation is available for part of your population and must degrade to "unknown" for the rest rather than being assumed absent.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering — turning a field percentile into a service level objective, and deciding what happens to the release train when the objective is missed.