Real User Monitoring

RUM Metrics: What to Measure and How to Interpret Real User Data

A comprehensive guide to Real User Monitoring metrics — from Core Web Vitals to custom business metrics — and how to interpret them to improve user experience.

AzMonitor TeamJuly 23, 20259 min read · 1,524 wordsUpdated January 20, 2026
RUM metricsCore Web Vitalsweb performanceuser experience

Real User Monitoring produces a lot of data. The challenge isn't collecting metrics — it's knowing which ones matter and how to interpret what you see. Not all RUM metrics have equal impact on user experience or business outcomes. Focusing on the right metrics, with the right targets, and understanding what drives them is what separates useful RUM from a dashboard nobody looks at.

The Metric Hierarchy

Think of RUM metrics in three tiers:

Business metrics — Conversion rate, session duration, bounce rate. These are what ultimately matter but are hard to directly optimize.

User experience metrics — Core Web Vitals (LCP, CLS, INP). These directly reflect how users perceive your site.

Technical metrics — TTFB, DOM loading, resource timing. These explain why the user experience metrics look the way they do.

Start with business metrics to establish that performance matters. Then focus on user experience metrics to guide improvement. Use technical metrics to diagnose root causes.

Core Web Vitals in Depth

Largest Contentful Paint (LCP)

LCP measures when the largest visible element finishes loading. This is usually your hero image, main heading, or primary content block.

Good: ≤ 2500ms Needs improvement: 2500ms - 4000ms Poor: > 4000ms

Common LCP elements and how to identify them:

// Find what element is LCP on your pages
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log({
      element: entry.element,           // The DOM element
      size: entry.size,                 // Element size (px)
      renderTime: entry.renderTime,     // When it rendered
      loadTime: entry.loadTime,         // When it loaded
      url: entry.url,                   // For image/video LCP
      tagName: entry.element?.tagName  // img, video, div, etc.
    });
  }
});

observer.observe({ type: 'largest-contentful-paint', buffered: true });

LCP breakdown by cause:

| LCP Element Type | % of Pages | Primary Fix | |---|---|---| | Image | 70% | Compress, use WebP, add fetchpriority="high" | | Text block | 20% | Reduce TTFB, eliminate render-blocking resources | | Video poster | 5% | Optimize poster image | | SVG | 5% | Inline or preload |

Cumulative Layout Shift (CLS)

CLS measures how much the page layout shifts after initial render. A score of 0.1 or below is good.

// Track CLS sources
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {  // Exclude user-triggered shifts
      for (const source of entry.sources) {
        console.log({
          element: source.node,
          previousRect: source.previousRect,
          currentRect: source.currentRect,
          shift_score: entry.value
        });
      }
    }
  }
});

observer.observe({ type: 'layout-shift', buffered: true });

The most common CLS causes:

  • Images without explicit dimensions (width and height attributes)
  • Ads, embeds, or iframes without reserved space
  • Fonts causing FOUT (Flash of Unstyled Text)
  • Dynamically injected content above existing content

Interaction to Next Paint (INP)

INP replaced FID (First Input Delay) as the Core Web Vital for responsiveness. It measures the worst interaction latency throughout the user's session.

Good: ≤ 200ms Needs improvement: 200ms - 500ms Poor: > 500ms

// Track all interactions for INP analysis
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 200) {  // Track slow interactions
      console.log({
        name: entry.name,           // 'click', 'keydown', etc.
        duration: entry.duration,  // Total interaction time
        processingStart: entry.processingStart - entry.startTime,  // Input delay
        processingEnd: entry.processingDuration,    // Processing time
        renderDelay: entry.duration - entry.processingEnd,   // Render delay
        target: entry.target
      });
    }
  }
});

observer.observe({ type: 'event', durationThreshold: 16, buffered: true });

Navigation Timing Metrics

Navigation timing breaks down the page load into discrete phases:

DNS lookup → TCP connect → TLS handshake → Request → Response → DOM processing → Load
    |              |              |           |           |              |           |
   dns            tcp           ssl         ttfb        download     domLoad    fullLoad

Understanding each phase:

| Phase | Typical Good | What Causes It to Slow | |---|---|---| | DNS lookup | < 5ms (cached) to 50ms | DNS propagation issues, slow resolver | | TCP connect | < 20ms | Server geography, network route | | TLS handshake | < 50ms | Certificate validation, protocol version | | TTFB | < 200ms | Server processing, database queries | | Download | < 100ms | Response size, bandwidth | | DOM processing | < 500ms | JavaScript execution, DOM size |

Resource Timing: Where Is Time Being Spent?

Resource timing shows how long each resource takes to load — images, scripts, stylesheets, fonts, API calls:

// Analyze resource loading performance
function analyzeResourceTiming() {
  const resources = performance.getEntriesByType('resource');
  
  // Group by resource type
  const byType = {};
  
  for (const resource of resources) {
    const type = resource.initiatorType;  // 'script', 'img', 'css', 'fetch', etc.
    
    if (!byType[type]) {
      byType[type] = {
        count: 0,
        totalSize: 0,
        totalDuration: 0,
        slowest: null
      };
    }
    
    byType[type].count++;
    byType[type].totalSize += resource.transferSize;
    byType[type].totalDuration += resource.duration;
    
    if (!byType[type].slowest || resource.duration > byType[type].slowest.duration) {
      byType[type].slowest = {
        url: resource.name,
        duration: resource.duration,
        size: resource.transferSize
      };
    }
  }
  
  return byType;
}

// Example output analysis:
// {
//   "script": { count: 8, totalSize: 245000, slowest: { url: "/bundle.js", duration: 1200 } },
//   "img": { count: 12, totalSize: 890000, slowest: { url: "/hero.jpg", duration: 800 } },
//   "fetch": { count: 3, totalSize: 15000, slowest: { url: "/api/user", duration: 340 } }
// }

Session-Level Metrics

Beyond page-level metrics, track session-level performance:

Pages per session — Poor performance reduces pages viewed. A performance improvement that increases this metric directly increases engagement.

Session duration — Users with faster load times spend more time on site.

Rage clicks — Rapid clicks on an unresponsive element indicate poor INP or broken functionality.

Scroll depth — If users aren't scrolling, either content isn't relevant or the page is too slow to render what they came for.

// Track rage clicks (3+ clicks in 500ms)
class RageClickDetector {
  constructor(threshold = 3, window = 500) {
    this.threshold = threshold;
    this.window = window;
    this.clicks = [];
    
    document.addEventListener('click', (e) => this.track(e));
  }
  
  track(event) {
    const now = Date.now();
    this.clicks = this.clicks.filter(t => now - t < this.window);
    this.clicks.push(now);
    
    if (this.clicks.length >= this.threshold) {
      // Rage click detected
      this.report({
        element: event.target.tagName,
        elementId: event.target.id,
        elementClass: event.target.className,
        clickCount: this.clicks.length,
        url: window.location.href
      });
      this.clicks = [];
    }
  }
  
  report(data) {
    navigator.sendBeacon('/api/rum/rage-click', JSON.stringify(data));
  }
}

Setting Performance Budgets with RUM Data

Use your RUM data to set realistic performance budgets:

// Performance budget based on actual user data
const performanceBudget = {
  // Based on current p75 values from RUM
  lcp: {
    p75_target: 2500,     // ms - "good" threshold
    p90_target: 4000,     // ms - acceptable degradation limit
    alert_if_above: 4500  // ms - alert on regression
  },
  cls: {
    p75_target: 0.1,
    alert_if_above: 0.25
  },
  inp: {
    p75_target: 200,
    alert_if_above: 500
  },
  ttfb: {
    p95_target: 600,
    alert_if_above: 1000
  }
};

// Check if new deployment regressed performance
function checkPerformanceBudget(currentMetrics, budget) {
  const violations = [];
  
  for (const [metric, thresholds] of Object.entries(budget)) {
    const current = currentMetrics[metric];
    
    if (current > thresholds.alert_if_above) {
      violations.push({
        metric,
        current,
        threshold: thresholds.alert_if_above,
        severity: 'critical'
      });
    } else if (current > thresholds.p75_target) {
      violations.push({
        metric,
        current,
        threshold: thresholds.p75_target,
        severity: 'warning'
      });
    }
  }
  
  return violations;
}

Geographic Performance Analysis

Performance often varies significantly by region. Analyze your RUM data geographically:

-- Geographic performance analysis
SELECT
    country_code,
    COUNT(*) as page_views,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY lcp_ms) as lcp_p50,
    PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY lcp_ms) as lcp_p75,
    PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY ttfb_ms) as ttfb_p75,
    AVG(cls_score) as avg_cls,
    COUNT(CASE WHEN lcp_ms <= 2500 THEN 1 END) * 100.0 / COUNT(*) as lcp_good_pct
FROM rum_page_views
WHERE timestamp > NOW() - INTERVAL '30 days'
  AND page_views > 100  -- Exclude regions with insufficient data
GROUP BY country_code
HAVING COUNT(*) > 100
ORDER BY lcp_p75 DESC
LIMIT 20;

If TTFB is uniformly high for a region but LCP varies, CDN coverage is likely the issue. If both TTFB and LCP are high, the problem is server-side for that region.

Correlating Performance with Business Metrics

The ultimate purpose of RUM is understanding how performance affects your business:

-- Correlate LCP with conversion rate
SELECT
    CASE
        WHEN lcp_ms <= 2500 THEN 'Good (≤2.5s)'
        WHEN lcp_ms <= 4000 THEN 'Needs Work (2.5-4s)'
        ELSE 'Poor (>4s)'
    END as lcp_category,
    COUNT(*) as sessions,
    SUM(CASE WHEN converted THEN 1 ELSE 0 END) as conversions,
    SUM(CASE WHEN converted THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as conversion_rate
FROM user_sessions
JOIN rum_page_views USING (session_id)
WHERE page_type = 'checkout'
  AND timestamp > NOW() - INTERVAL '30 days'
GROUP BY lcp_category
ORDER BY lcp_ms;

When you can show that sessions with LCP > 4s have a 30% lower conversion rate, performance work becomes easy to prioritize.

Conclusion

RUM metrics are most valuable when they're connected to user outcomes and business results. The Core Web Vitals — LCP, CLS, and INP — are your primary user experience indicators. Navigation timing and resource timing explain why they're at their current levels. Geographic and device breakdowns show you who's most affected. And correlations with conversion rates and session metrics make the business case for optimization. Use AzMonitor's synthetic monitoring to catch performance regressions immediately after deployments, and RUM to validate that your performance improvements actually reach users in the real world with all their diverse conditions.

Tags:RUM metricsCore Web Vitalsweb performanceuser experience
Back to blog
A
AzMonitor Team
The AzMonitor team writes guides based on experience monitoring millions of endpoints daily across 10,000+ customer environments. Our expertise covers uptime monitoring, SRE practices, and reliability engineering.
Try AzMonitor free

3 monitors free forever · No credit card needed · Set up in 2 minutes

Start monitoring free →