Skip to content

Real User Monitoring (RUM) Explained: How It Works & When to Use It

Webalert Team
May 28, 2026
13 min read

Real User Monitoring (RUM) Explained: How It Works & When to Use It

Real User Monitoring (RUM) is the practice of measuring performance and errors as they happen, in real browsers, on real devices, in real networks - your actual users, not a synthetic robot in a US-East data center. It is how you find out that your site is fast on your MacBook over Ethernet and 6 seconds slow for a Pixel 6 on a 4G train in Berlin.

This guide explains RUM from first principles: what it measures, how the data gets from the browser to your dashboard, when to use it, when not to use it, and how it fits alongside synthetic monitoring. No vendor pitch, no jargon - just the model.

For the head-to-head with synthetic monitoring, see Synthetic vs Real User Monitoring. This post is the primary topic guide, not a comparison.


What RUM Is, In One Sentence

RUM is a JavaScript snippet that runs in every (or sampled) user's browser, listens to performance and error events the browser emits, and ships those measurements to a backend for aggregation.

That is it. Everything below is implementation detail.


What RUM Measures

A modern RUM implementation captures four families of signals:

1. Core Web Vitals

The Google-defined user-experience metrics that also influence search ranking.

  • LCP (Largest Contentful Paint) - when the largest above-the-fold element rendered. Loading speed.
  • INP (Interaction to Next Paint) - worst per-interaction delay across the session. Responsiveness. Replaced FID in 2024.
  • CLS (Cumulative Layout Shift) - how much things jumped around. Visual stability.

Google's "good" thresholds:

Metric Good Needs improvement Poor
LCP <= 2.5 s <= 4.0 s > 4.0 s
INP <= 200 ms <= 500 ms > 500 ms
CLS <= 0.1 <= 0.25 > 0.25

RUM is the only way to get the field data Google uses for ranking. Synthetic numbers are not what Search Console reports.

Deeper reads: Core Web Vitals Monitoring, INP Monitoring.

2. Navigation & resource timings

Via the Navigation Timing and Resource Timing APIs:

  • DNS, TCP, TLS, request, response, DOM processing, load.
  • Per-resource: which scripts, fonts, images blocked rendering and for how long.
  • TTFB (Time To First Byte) - server response time as the user experienced it, including their last-mile network.

See TTFB Monitoring.

3. JavaScript errors and unhandled rejections

Captured via window.addEventListener('error', ...) and window.addEventListener('unhandledrejection', ...). Includes stack traces, source-mapped if you upload sourcemaps.

4. Custom timings and user actions

performance.mark() and performance.measure() for app-specific milestones: "checkout button visible", "first product card painted", "search results returned". Combine with custom user-action events for funnel-level RUM.

Bonus signals modern RUM often adds:

  • TTI / FCP / FID legacy / TBT - older Web Vitals.
  • Long tasks - via PerformanceObserver on the longtask entry type.
  • Crash detection - via the pagehide / beforeunload events plus visibility heuristics.
  • Network type - via navigator.connection.effectiveType (4g, 3g, slow-2g).
  • Device info - device memory, CPU class, viewport size.

How RUM Actually Works

Under the hood, a RUM client does five things:

1. Load early, but lazily

The script needs to load early enough to catch initial paint events, but not so synchronously that it itself harms LCP. Common patterns:

  • A small inline bootstrapper (< 1 KB) that registers PerformanceObservers immediately.
  • The full RUM library lazy-loaded via requestIdleCallback or after load.
  • Buffered measurements queued until the library is ready.
<script>
  // Tiny inline RUM bootstrapper
  window.__rumQueue = [];
  ['largest-contentful-paint','layout-shift','first-input','event','longtask','navigation','resource']
    .forEach(function(type){
      try {
        new PerformanceObserver(function(list){
          window.__rumQueue.push.apply(window.__rumQueue, list.getEntries().map(function(e){
            return { type: type, entry: e.toJSON ? e.toJSON() : e };
          }));
        }).observe({ type: type, buffered: true });
      } catch (_) {}
    });
</script>
<script src="https://example.com/rum.js" async></script>

The full library reads window.__rumQueue, processes, and starts shipping.

2. Observe via PerformanceObserver

PerformanceObserver is the canonical browser API for performance events. Important properties:

  • buffered: true replays events that fired before the observer attached.
  • Each entryType requires its own observer or a single multi-type observer in modern browsers.
  • Entries are immutable; copy what you need.

3. Aggregate per page-view session

A RUM session is "this page view in this tab". Track:

  • A view_id (UUID per navigation).
  • A session_id (sticky for tab + first-party storage TTL, e.g. 30 min idle).
  • Aggregated worst-case INP, final CLS, definitive LCP - because these metrics are final-on-pagehide metrics, not single events.

4. Beacon on pagehide / visibilitychange: hidden

Do not ship measurements on beforeunload (unreliable on mobile, deprecated on Safari). Instead:

  • Subscribe to visibilitychange and check document.visibilityState === 'hidden'.
  • Also handle pagehide for legacy browsers.
  • Use navigator.sendBeacon(url, body) so the request survives the page unload.
function flush() {
  const payload = JSON.stringify(buildBatch());
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/rum/ingest', payload);
  } else {
    fetch('/rum/ingest', { method: 'POST', body: payload, keepalive: true });
  }
}

document.addEventListener('visibilitychange', function () {
  if (document.visibilityState === 'hidden') flush();
});
window.addEventListener('pagehide', flush);

keepalive: true is the modern alternative for browsers without sendBeacon for the payload type.

5. Sample at the edge

RUM data is high-volume. Production sites do not ship 100% of sessions:

  • Session-level sampling: decide at session start whether this session ships, e.g. 10% of users.
  • Outlier amplification: always ship sessions with errors or with metrics in the "poor" bucket so you do not miss tail problems.
  • Per-route sampling: sample high-traffic routes at 1%, low-traffic ones at 100% for statistical adequacy.

Sampling at 100% might cost you more than your application backend. Sampling at 0.1% will give you confidence intervals you cannot use. 1-10% is the practical band for most sites.


A Minimal RUM Beacon Payload

What ships per page view, schematically:

{
  "view_id": "01HV3...",
  "session_id": "01HV0...",
  "url": "/checkout/shipping",
  "ref": "/checkout/cart",
  "ts": 1748419500000,
  "ua": "Mozilla/5.0 ...",
  "device": { "memory_gb": 4, "cpu": 4 },
  "conn": { "type": "4g", "rtt": 90, "downlink": 4.2 },
  "geo_hint": { "country": "DE" },
  "metrics": {
    "ttfb_ms": 240,
    "fcp_ms": 1100,
    "lcp_ms": 2300,
    "inp_ms": 180,
    "cls": 0.05,
    "long_tasks_count": 3,
    "longest_task_ms": 90
  },
  "errors": [
    { "type": "TypeError", "msg": "Cannot read prop x of undefined", "src": "app.js", "ln": 4123, "col": 12 }
  ],
  "marks": {
    "checkout_button_visible_ms": 1450,
    "payment_form_interactive_ms": 1900
  }
}

Keep payloads small. Aim for < 5 KB compressed. Beacons over sendBeacon are typically capped to ~64 KB.


Aggregation: What To Compute, Not Just Average

Averages on web-perf data are misleading because the distribution is fat-tailed. Use percentiles, always.

Standard cuts:

  • p50 (median) - what a typical user sees.
  • p75 - the Google-reported value for Core Web Vitals (CrUX uses p75).
  • p95 - the user you are accidentally breaking.
  • p99 - the user you are definitely breaking.

Segment by:

  • Country / region - because last-mile networks differ massively.
  • Device class - low-RAM mobile vs desktop.
  • Effective connection type - 4g vs 3g vs slow-2g.
  • Route / page template - /products/[id] may be fine; /checkout/shipping may not.
  • Release / commit SHA - so you can see the moment a deploy regressed perf.

A useful default dashboard cell: "Checkout page, mobile, 4G, EU, p75 LCP, last 7 days, by release".


When To Use RUM

Choose RUM when:

  • You care about real-world user experience, especially mobile + emerging-market networks.
  • You need Core Web Vitals data Google actually uses for ranking.
  • You ship multiple releases per day and need per-release perf attribution.
  • You want to understand the long tail - the 5% of users with a bad time.
  • You need to correlate JavaScript errors with the performance state that caused them.

When To Use Synthetic Instead (Or Both)

Choose synthetic when:

  • You need uptime / SLA monitoring - RUM cannot tell you when nobody could reach your site, because there are no real users to report.
  • You need pre-production / staging perf checks.
  • You need consistent, controlled baseline numbers across releases.
  • Your traffic is too low for RUM to be statistically useful (a B2B tool with 50 daily users will not have RUM signal at 10% sampling).
  • You want to validate response content, not just timing. See Response Body Validation Monitoring.

RUM tells you what your users experienced. Synthetic tells you whether your site is up and whether it would work for a new user. Both have blind spots only the other fills.

For the side-by-side, see Synthetic vs Real User Monitoring.


Privacy & Compliance

RUM ships data from the user's browser. That triggers privacy law on three axes:

IP addresses

  • Often used for geolocation hints.
  • Truncate at ingest (e.g. zero the last octet of IPv4 or last 80 bits of IPv6) before storing.
  • Some EU regulators consider truncated IPs non-personal data; others disagree. Get legal sign-off.

User identifiers

  • Do not ship user_id, email, or session tokens unless your privacy policy and consent flow allow it.
  • Prefer anonymous session_id rotated on logout.
  • Many RUM implementations are cookieless (sessionStorage / first-party random IDs) and therefore exempt from most cookie consent rules.
  • If you store anything persistently, your consent banner must include RUM.
  • See Cookie Consent Monitoring.

Personal data in URLs

  • Many apps put email or tokens in query strings.
  • Strip them at the client before beaconing, or at the ingest edge.

Cross-border data transfer

  • If you self-host RUM ingest, route it to a region your privacy policy commits to.
  • If you use a vendor, verify their data residency.

Geographic / GDPR opt-out

  • Some teams disable RUM for users in jurisdictions that have not granted consent, accepting reduced sample size for legal cleanliness.

A reasonable default: cookieless, IP-truncated, no PII in payload, per-region routing, disclosed in privacy policy.


Acting On RUM Data: A Playbook

Data without action is just noise. The repeatable RUM workflow:

  1. Set targets per route, not site-wide. "Checkout p75 LCP < 2.5 s" is actionable; "Site LCP good" is not.
  2. Alert on regression deltas, not absolutes. A 30% jump in p75 LCP on a single route after a deploy is a regression even if it is still in the "good" band.
  3. Correlate with release SHA so the post-mortem starts at "which deploy?"
  4. Filter by long-task count to find scripts that block the main thread.
  5. Cross-reference errors - an INP spike often coincides with an error storm.
  6. Drill into resource timings to see whether a third-party script is the culprit.
  7. Validate the fix in the next release: p75 should come back down, and the long tail (p95, p99) should also improve, not just the median.

Architectural Decisions

If you are building or buying RUM, these are the decisions that matter:

Decision Implications
Vendor vs self-hosted Vendor = fast time-to-value, monthly cost scaling with volume. Self-hosted = data ownership, more ops.
Sampling rate Cost vs statistical confidence. Start at 10%, adjust per route.
Sourcemap upload Required for usable JS error stacks. Automate in CI.
Ingest endpoint location Choose a region matching your audience for low beacon latency.
Payload shape Schema-versioned, forward-compatible. RUM payloads outlive their client library.
Storage Time-series + per-session log. Cardinality matters - segment fields are dimensions, not tags.
Retention Daily aggregates for years, raw sessions for 7-30 days.
Edge collection If you can ingest at the CDN edge, do - cuts beacon RTT and offloads origin.

A Reference Dashboard

For each top-priority route:

  • Core Web Vitals: p75 LCP, p75 INP, p75 CLS - current vs last release vs target.
  • Distribution: histogram of LCP for the last 24h, "good / needs-improvement / poor" stack.
  • Geography: choropleth or table of p75 LCP by country.
  • Network: p75 LCP by effectiveType.
  • Errors: top JS errors with affected sessions count, last 24h.
  • Long tasks: count and longest task per route.
  • Release timeline: p75 LCP overlaid with release markers.

Pair with synthetic uptime dashboards from Webalert features so you have both "is it up?" and "is it fast for real users?" in one place.


RUM Checklist

  • Web Vitals (LCP, INP, CLS) captured with PerformanceObserver
  • Navigation + resource timings collected
  • JS errors with sourcemap symbolication
  • Custom marks for app-critical milestones
  • Beacons sent on visibilitychange: hidden + pagehide, via sendBeacon
  • Sampling configured per route, with outlier amplification
  • Payload size under 5 KB compressed
  • IP truncation and PII stripping at ingest
  • Cookieless or properly consented
  • Per-release attribution (commit SHA in payload)
  • Percentile-based dashboards (p50/p75/p95), not averages
  • Alerts on regression deltas per route, not site-wide
  • Paired with synthetic uptime + content-validation monitoring

How Webalert Helps

RUM is the inside-out view of your site. Webalert is the outside-in view that catches the failures RUM cannot see:

  • Uptime monitoring from real regions - because RUM cannot report when nobody can load the page.
  • Page-load timing - synthetic LCP/TTFB for consistent baselines across releases.
  • Content validation - catch the day RUM keeps reporting "fast pages" that are actually a 200-OK error page. See Response Body Validation.
  • TLS / DNS / domain monitoring - the categorical reasons RUM goes silent.
  • JavaScript rendering monitoring - what real browsers actually render. See JavaScript SEO & Googlebot Rendering.

A useful pairing:

  1. RUM tells you LCP regressed for German Pixel 6 users after release X.
  2. Webalert tells you those users got a 200 OK and your CDN cache is healthy.
  3. Conclusion: bundle size or client-side script regression, not infrastructure. Time-to-RCA: minutes.

Summary

RUM is the only honest way to know how fast your site is for the people using it. Capture Web Vitals, navigation timings, errors, and custom marks via PerformanceObserver. Beacon on visibilitychange: hidden. Sample. Strip PII. Use percentiles. Alert on regression deltas. And pair RUM with synthetic monitoring so you have both inside-out and outside-in coverage.

The teams that get this right have one shared trait: they can answer "did the last deploy hurt user experience?" in five minutes, with data, broken down by route and device class. If you cannot answer that today, RUM is the missing piece.


Pair your RUM with outside-in uptime monitoring

Start monitoring with Webalert ->

See features and pricing. No credit card required.

Monitor your website in under 60 seconds — no credit card required.

Start Free Monitoring

Written by

Webalert Team

The Webalert team is dedicated to helping businesses keep their websites online and their users happy with reliable monitoring solutions.

Ready to Monitor Your Website?

Start monitoring for free with 3 monitors, 10-minute checks, and instant alerts.

Start Free Monitoring