Building a RUM Telemetry Pipeline for Video Players: Beacons, Batching, and Sampling That Scales

How to collect real-user metrics from video players without overwhelming your servers — beacon batching, session sampling, and the events worth emitting.

Real-user monitoring for video players is deceptively simple in theory — “just send events” — and deceptively hard in practice. A million concurrent viewers emitting events every second is a firehose that will overwhelm your ingest layer, inflate your bill, and still tell you less than a well-designed sampled feed.

The Event Taxonomy

Not every event is worth a beacon. Organize into three tiers:

TierEventsCadence
Criticalsession_start, video_start, video_start_fail, session_end, fatal_errorAlways emit
Qualityrebuffer_start, rebuffer_end, bitrate_switch, join_timeAlways emit (they’re the QoE core)
Diagnosticseek, pause, quality_request, drm_requestSampled or batched

The critical and quality tiers carry your dashboards. Diagnostic tier is for deep dives — sample it at 10–20% of sessions or your event volume triples for marginal insight.

Batching: The Only Way It Scales

One beacon per event is architecturally wrong at scale. Buffer events client-side and flush on:

  • A time interval (every 15–30s) — bounds staleness
  • A size threshold (e.g., 10 events) — bounds payload
  • Critical events — always emit immediately, no buffering
  • Session end — a final sendBeacon flush so you don’t lose the tail
class TelemetryBatcher {
  private queue: PlaybackEvent[] = [];
  private timer: ReturnType<typeof setInterval>;

  constructor(private endpoint: string) {
    this.timer = setInterval(() => this.flush(), 15_000);
  }

  emit(e: PlaybackEvent) {
    if (e.critical) return this.send([e]); // no buffering for criticals
    this.queue.push(e);
    if (this.queue.length >= 10) this.flush();
  }

  flush() {
    if (!this.queue.length) return;
    const batch = this.queue.splice(0);
    navigator.sendBeacon(this.endpoint, JSON.stringify(batch));
  }
}

navigator.sendBeacon is the right API — it’s fire-and-forget, survives page unload, and doesn’t block the main thread on the way out.

Sampling Without Losing the Signal

Full sampling on high-traffic platforms is expensive; no sampling on small ones is noise-free but wasteful. The standard pattern: session-level sampling, not event-level. A session is either fully instrumented or fully dark — per-event sampling breaks causality chains (“was the rebuffer caused by a bitrate switch?”) because you can’t reconstruct the session.

“Sample sessions, not events. A 5% session sample gives you clean causal chains; a 50% event sample gives you a pile of disconnected data points you can’t replay.”

Our event schema, batching thresholds, and the end-to-end pipeline (player → edge ingest → stream processing → dashboard) are in the RUM telemetry pipeline for video.