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:
| Tier | Events | Cadence |
|---|---|---|
| Critical | session_start, video_start, video_start_fail, session_end, fatal_error | Always emit |
| Quality | rebuffer_start, rebuffer_end, bitrate_switch, join_time | Always emit (they’re the QoE core) |
| Diagnostic | seek, pause, quality_request, drm_request | Sampled 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
sendBeaconflush 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.