Skip to content
Back to Operations & SRE

Real User Monitoring

Collect performance and errors from real user browsers to see what users actually experience.

Why

Synthetic monitoring checks a known path from a clean browser; RUM checks every real user on every real device, network, and geography, which is the ground truth of user experience. A synthetic that passes at 200ms from a data center tells you nothing about the mobile user on 3G in Brazil who waits 8 seconds for your JS bundle to parse. RUM captures the long tail — the slowest 1% of users who never file a bug but just leave. It's also the only signal that ties technical performance to business outcomes: when you can show that p75 LCP correlates with checkout abandonment, you get funding to fix frontend performance that synthetic alone never justifies.

When

For any user-facing web or mobile app in production. Start with Core Web Vitals (LCP, FID/INP, CLS) + JS errors; add custom timings for key flows.

How

1. Pick an SDK and a backend
   - Browser: web-vitals npm pkg + Beacon API → your collector / Datadum/Sentry/New Relic Browser
   - Mobile: native SDK (Firebase Perf, Datadog RUM) — don't try to polyfill web vitals

2. Capture the Core Web Vitals + JS errors
   - LCP (Largest Contentful Paint): when the main content renders — target <2.5s p75
   - INP (Interaction to Next Paint): input responsiveness — target <200ms p75
   - CLS (Cumulative Layout Shift): visual stability — target <0.1 p75
   - window.onerror + unhandledrejection for JS errors with stack + release tag

3. Tag every event with user context (privacy-safe)
   - Country, device, browser, connection type, page route, app version
   - NOT user_id or PII — RUM is sampled and aggregated, not per-user surveillance

4. Build a RUM dashboard keyed on p75 and p95, not averages
   - "p75 LCP by route" — which pages are slow for the 25% slowest users
   - "JS error rate by release" — did the latest deploy regress?
   - "LCP by country/device" — is a specific segment suffering?

5. Set SLOs on RUM and alert on burn rate
   - "p75 LCP < 2.5s for 99% of 28-day windows" → alert if burning >2x
   - Tie to business: correlate LCP regression with checkout conversion

Example

// Capture Core Web Vitals + JS errors, ship to collector
import { onLCP, onINP, onCLS } from 'web-vitals';

function send(metric) {
  navigator.sendBeacon('/rum/collect', JSON.stringify({
    name: metric.name, value: metric.value, id: metric.id,
    route: location.pathname, ts: Date.now(),
    country: '<%= request.country %>', // edge-injected
    release: __APP_VERSION__,
  }));
}
onLCP(send); onINP(send); onCLS(send);
window.addEventListener('error', e =>
  send({ name: 'js_error', value: 1, id: e.message + '@' + e.lineno }));
// Dashboard query (Datadog RUM):
//   p75(lcp) by route where service=web  →  find slow pages
//   count(js_error) by release           →  did deploy regress?

Common Problems

  • ⚠Reporting average LCP instead of p75/p95 — the average looks fine while 25% of users have a terrible experience; Google ranks on p75, not mean.
  • ⚠Sending RUM on every page view without sampling — at 1M daily visits you ingest 1M events/day and blow your budget; sample 10-20% for perf, 100% for errors.
  • ⚠Tagging RUM with user_id or email — creates a PII retention and GDPR problem; RUM should be anonymous and aggregated.
  • ⚠Ignoring mobile and slow-network users — a dashboard that only shows desktop fiber users misses the segment with the worst experience.

Best Practices

  • ✓Report p75 and p95 of Core Web Vitals, not averages — these are what Google ranks on and what the slowest users actually feel.
  • ✓Sample RUM (10-20% for perf metrics, 100% for JS errors) to control cost and ingestion volume without losing signal on rare errors.
  • ✓Correlate RUM regressions with business metrics (conversion, bounce) — this is how you justify performance work to product teams.
  • ✓Segment RUM by device, country, and connection type so you can see whether a regression is global or specific to one user segment.