Skip to content

Monitoring Web Applications: RUM and Synthetic Monitoring

DodaTech Updated 2026-06-23 7 min read

In this tutorial, you'll learn about Monitoring Web Applications: RUM and Synthetic Monitoring. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

What You Will Learn

This tutorial teaches you how to monitor web application performance using Real User Monitoring (RUM) for actual visitor data and synthetic monitoring for proactive testing, covering Core Web Vitals, page load metrics, and API performance.

Why It Matters

Users judge your application by how fast it loads and responds. A one-second delay in page load time can reduce conversions by 7%. Without web monitoring, you are unaware of performance problems that directly impact user experience and revenue.

Real-World Use

The Doda Browser team uses RUM to track Core Web Vitals across all browser versions. When a new release caused a 20% regression in First Input Delay, the RUM data caught it within hours of the rollout, and the team reverted the offending change before the weekly release cycle.

Web application monitoring splits into two categories: Real User Monitoring (RUM) captures data from actual users loading your site in their browsers. Synthetic monitoring runs scripted tests from controlled environments to proactively detect issues before users do. Both are essential for a complete picture.


Prerequisites

  • A web application to monitor (your own or a test site)
  • Node.js 18+ installed
  • A Linux server or local development machine
  • Basic knowledge of HTML and JavaScript

Step-by-Step Tutorial

Step 1: Set Up a Simple Web Application

Create index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DodaTech Demo App</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Welcome to DodaTech</h1>
    <button id="load-data">Load Data</button>
    <div id="content"></div>
    <script src="app.js"></script>
</body>
</html>

Create app.js:

document.getElementById("load-data").addEventListener("click", async () => {
    const start = performance.now();
    const response = await fetch("/api/data");
    const data = await response.json();
    const duration = performance.now() - start;
    document.getElementById("content").textContent =
        `Loaded ${data.items} items in ${duration.toFixed(0)}ms`;
});

Step 2: Add RUM with the Performance API

// Send performance data to your analytics endpoint
function sendPerformanceMetrics() {
    if (!performance.getEntriesByType) return;

    const navigation = performance.getEntriesByType("navigation")[0];
    const paint = performance.getEntriesByType("paint");

    const metrics = {
        dns: navigation.domainLookupEnd - navigation.domainLookupStart,
        tcp: navigation.connectEnd - navigation.connectStart,
        ttfb: navigation.responseStart - navigation.requestStart,
        dom_load: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
        full_load: navigation.loadEventEnd - navigation.loadEventStart,
        fcp: paint.find(p => p.name === "first-contentful-paint")?.startTime,
    };

    navigator.sendBeacon("/api/rum", JSON.stringify(metrics));
}

window.addEventListener("load", sendPerformanceMetrics);

Step 3: Collect Web Vitals with the web-vitals Library

npm install web-vitals
import { onCLS, onFID, onLCP, onINP } from "web-vitals";

function sendToAnalytics({ name, value, rating }) {
    const body = JSON.stringify({ name, value, rating, url: location.href });
    navigator.sendBeacon("/api/vitals", body);
}

onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics);

Step 4: Create a RUM Collection Endpoint

from flask import Flask, request
import json
import time

app = Flask(__name__)

@app.route("/api/rum", methods=["POST"])
def collect_rum():
    data = request.get_json()
    data["timestamp"] = time.time()
    print(f"RUM data: {json.dumps(data)}")
    return {"status": "ok"}

@app.route("/api/vitals", methods=["POST"])
def collect_vitals():
    data = request.get_json()
    data["timestamp"] = time.time()
    print(f"Web Vital: {json.dumps(data)}")
    return {"status": "ok"}

if __name__ == "__main__":
    app.run(port=5000)

Step 5: Set Up Synthetic Monitoring with Playwright

npm init -y
npm install @playwright/test
npx playwright install chromium

Create synthetic-test.js:

const { chromium } = require("@playwright/test");

(async () => {
    const browser = await chromium.launch();
    const page = await browser.newPage();

    const start = Date.now();
    await page.goto("https://your-app.dodatech.com", { waitUntil: "networkidle" });
    const loadTime = Date.now() - start;

    const metrics = await page.evaluate(() => ({
        lcp: performance.getEntriesByType("paint")
            .find(p => p.name === "first-contentful-paint")?.startTime,
        cls: performance.getEntriesByType("layout-shift")
            ?.reduce((sum, e) => sum + e.value, 0),
    }));

    console.log({ loadTime, ...metrics });
    await browser.close();
})();

Step 6: Run Lighthouse CI for Performance Scoring

npm install -g @lhci/cli

Create .lighthouserc.json:

{
    "ci": {
        "collect": {
            "url": ["https://your-app.dodatech.com"],
            "numberOfRuns": 3
        },
        "assert": {
            "assertions": {
                "categories:performance": ["warn", { "minScore": 0.9 }],
                "categories:accessibility": ["error", { "minScore": 0.95 }],
                "categories:seo": ["error", { "minScore": 0.9 }]
            }
        }
    }
}
lhci autorun

Expected output: A Lighthouse report with performance, Accessibility, best practices, and SEO scores.

Step 7: Set Up API Monitoring

const https = require("https");

function checkEndpoint(url, expectedStatus = 200) {
    const start = Date.now();
    https.get(url, (res) => {
        const duration = Date.now() - start;
        const status = res.statusCode === expectedStatus ? "pass" : "fail";
        console.log(JSON.stringify({
            url, status, duration_ms: duration, timestamp: new Date().toISOString()
        }));
    });
}

setInterval(() => {
    checkEndpoint("https://api.dodatech.com/health");
    checkEndpoint("https://api.dodatech.com/v1/users");
}, 60000);

Step 8: Alert on Web Performance Degradation

# RUM: FCP above 2.5 seconds
avg by (page) (rum_first_contentful_paint) > 2500

# Synthetic: Lighthouse performance score below 85
lighthouse_performance_score < 85

# Synthetic: API response time above 500ms
avg by (endpoint) (synthetic_api_duration_ms) > 500

Learning Path

flowchart LR
    A[Web App Monitoring] --> B[Real User Monitoring]
    A --> C[Synthetic Monitoring]
    B --> D[Core Web Vitals]
    B --> E[Page Load Metrics]
    B --> F[API Performance]
    C --> G[Playwright Tests]
    C --> H[Lighthouse CI]
    C --> I[API Health Checks]
    D --> J[Dashboards & Alerts]
    G --> J
    H --> J
    style A fill:#4a90d9,color:#fff
    style J fill:#e67e22,color:#fff

Common Errors

  1. RUM data is blocked by ad blockers -- Ad blockers may block analytics endpoints. Use /api/rum paths that do not match common block patterns.

  2. Web Vitals library returns null for some metrics -- The browser does not support certain metrics or the interaction did not occur. Only LCP is guaranteed; CLS and INP require user interaction.

  3. Playwright test times out -- The page does not reach networkidle due to persistent connections. Change wait strategy to domcontentloaded.

  4. Lighthouse CI reports lower scores than local tests -- The CI environment has fewer resources. Use the same hardware class as your production deployment for consistent results.

  5. CORS errors from RUM endpoints -- The endpoint does not have proper CORS headers. Add Access-Control-Allow-Origin: * to the collection endpoint.

  6. Synthetic tests fail due to dynamic content -- The test expects specific text that changed after a deployment. Use stable selectors instead of text content.

  7. Performance metrics show high variance -- Network conditions and device capabilities vary. Collect enough samples (100+) before drawing conclusions.


Practice Questions

  1. What is the difference between RUM and synthetic monitoring? Answer: RUM captures data from real users in production. Synthetic monitoring runs controlled tests from test environments to detect issues before users do.

  2. What are Core Web Vitals? Answer: LCP (Largest Contentful Paint), FID (First Input Delay), and CLS (Cumulative Layout Shift) -- Google metrics that measure loading, interactivity, and visual stability.

  3. How does the Performance API help with RUM? Answer: It provides timing data for DNS, TCP, TTFB, DOM loading, and paint events through performance.getEntriesByType().

  4. What is the purpose of navigator.sendBeacon() in RUM? Answer: It sends data to the server asynchronously without delaying page unload, ensuring metrics are captured even when the user navigates away.

  5. How does Lighthouse CI integrate into a deployment pipeline? Answer: It runs Lighthouse audits as part of CI, asserting performance budgets and preventing regressions before they reach production.


Challenge

Build a complete web monitoring stack for a single-page application. Add RUM instrumentation that captures LCP, CLS, and INP metrics and sends them to a collection endpoint. Deploy a synthetic monitoring script using Playwright that navigates through the three most important user flows (login, search, checkout) and reports timing for each step. Configure Lighthouse CI to run on every deployment and enforce a performance budget of 90+. Create a Grafana dashboard that shows: RUM FCP trend, synthetic page load time, Lighthouse score trend, and API error rate. Set up alerts for FCP exceeding 2.5 seconds and Lighthouse score dropping below 85.


FAQ

Can I use RUM without a third-party service?

Yes, you can build your own RUM collection endpoint with any backend language. Send metrics via navigator.sendBeacon() and store them in your database.

Is synthetic monitoring a replacement for RUM?

No. Synthetic monitoring is proactive and controlled. RUM reflects actual user experiences. Both are necessary for complete web monitoring.

What is a good FCP score?

Google considers FCP under 1.8 seconds good, between 1.8 and 3.0 needs improvement, and above 3.0 poor.

Does Lighthouse CI work with single-page applications?

Yes, but you may need to wait for specific elements to render rather than relying on networkidle.

How do I measure API performance from the browser?

Use the Performance API's getEntriesByType("resource") to measure fetch/XMLHttpRequest timings, or instrument your API calls manually.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro