RUM Time-Series Storage: Sentry, Datadog RUM and Grafana Faro

Real User Monitoring (RUM) differs fundamentally from backend monitoring in its data model: RUM collects event streams with massive high-cardinality attributes — every user visit generates session_id, user_id, page_url, and hundreds of dimensions. Traditional TSDB label-set models degrade severely under this pattern.

This article analyzes the time-series storage design of three mainstream RUM solutions: Sentry (open-source all-in-one), Datadog RUM (SaaS benchmark), and Grafana Faro (Grafana LGTM ecosystem extension). The common trend across all three is leveraging columnar OLAP instead of TSDB.

Quick Reference — What is RUM?

RUM is like having an invisible surveyor on every user’s phone, recording the wait time for every click and every frame of rendering delay. It automatically tracks every step of the user’s visit:

  • Page loading slow? → LCP (Largest Contentful Paint) tells you
  • Layout suddenly shifting? → CLS (Cumulative Layout Shift) tells you
  • Click not responding? → INP (Interaction to Next Paint) tells you

Unlike backend monitoring, RUM cares about the real end-to-end user experience — not just that the server responded in 200ms, but the actual time from a user’s click to the page being fully rendered.

RUM Data Collection Pipeline

The complete RUM data collection chain starts from the user’s browser, goes through SDK collection and network reporting, and finally lands in backend storage. While Sentry, Datadog, and Grafana Faro differ in their storage approaches, the front half of the pipeline is highly consistent.

RUM Data Collection PipelineUser Browser → Performance APIPage load triggers Navigation / Resource / Paint TimingWeb Vitals SDK CollectionLCPCLSINPAutomatically tracks Web Vitals per pagesendBeacon / fetch Batch ReportAuto-sends on page unload for reliable deliveryCollector → Columnar OLAP StorageClickHouse / Husky / Loki+Tempo etc.RUM pipeline: Page load → SDK collection (LCP/CLS/INP) → Report → Columnar storageAll mainstream RUM solutions share this collection paradigm; storage backend differs

Animation description: The animation above progressively reveals the complete RUM data pipeline from user browser to backend storage. First, page load triggers Performance API events (blue); the Web Vitals SDK automatically captures LCP, CLS, INP metrics (orange); data is batch-reported via sendBeacon or fetch (purple); finally it lands in columnar OLAP storage (green). All mainstream RUM solutions share this collection paradigm, differing only in backend storage selection.

Sentry

Data Flow Architecture

Sentry’s data pipeline follows a multi-stage stream processing architecture. From the browser SDK to final aggregated queries, events traverse the following path[R1]:

mermaid
flowchart TD
    A["Browser SDK"] -->|HTTP POST| B["Nginx<br/>Reverse Proxy"]
    B -->|proxy pass| C["Relay<br/>Validation + Rate Limit"]
    C -->|produce| D["Kafka<br/>envelope topic"]
    D -->|consume| E["IngestConsumer<br/>Preprocess + Symbolicate"]
    E -->|store raw| F["Nodestore<br/>PostgreSQL"]
    E -->|produce| G["Kafka<br/>events topic"]
    G -->|consume| H["SnubaConsumer<br/>Batch Write"]
    H --> I["ClickHouse<br/>Columnar + Time Partition<br/>10-30x Compression"]

    style A fill:#2196F3,color:#fff
    style B fill:#FF9800,color:#fff
    style C fill:#FF9800,color:#fff
    style D fill:#9C27B0,color:#fff
    style E fill:#FF9800,color:#fff
    style F fill:#795548,color:#fff
    style G fill:#9C27B0,color:#fff
    style H fill:#FF9800,color:#fff
    style I fill:#4CAF50,color:#fff

Color coding: blue = client, orange = stream processors, purple = Kafka message bus, brown = persistent storage, green = final OLAP engine. IngestConsumer is the critical fork point — raw payloads go to Nodestore (Postgres) for long-term archival, while structured events flow through the Kafka events topic → SnubaConsumer → ClickHouse pipeline for real-time analytics.

Storage Matrix[R1]:

StoragePurpose
PostgreSQLNodestore (raw event payload), Sentry business model
ClickHouse (via Snuba)Primary OLAP for event search, spans, performance metrics, aggregation queries
RedisCelery task queue, hot issue cache, rate-limit counters
KafkaEvent stream bus
Blob StorageAttachments, Replay segments, symbol files

Snuba: ClickHouse Abstraction Layer

Snuba is Sentry’s self-developed event storage and query service backed by ClickHouse, replacing the earlier Postgres-based Search/Tagstore and Redis-based TSDB[R5].

Why migrate from Postgres/Redis to ClickHouse[R5]:

  1. Data is largely immutable; MVCC safety mechanisms became a performance liability
  2. Adding new query dimensions required Postgres denormalization and full backfill, taking months
  3. Expired data cleanup required expensive bulk deletes

Snuba design highlights[R5]:

  • Read-write separation: queries and writes go to different ClickHouse clusters
  • Batch writes: batch commits reduce merge pressure
  • Time partitioning: drop partitions directly for expired data
  • PREWHERE clause: pre-filter skips irrelevant data blocks
  • Redis query cache: hot query results cached

ClickHouse columnar storage + primary key sorting compressed Tagstore data from TB-scale to GB-scale — columnar compression’s natural advantage on high-cardinality tags.

Issue Deduplication and Grouping

Sentry’s deduplication is an engineering highlight. Three systems run in sequence[R2]:

  1. normalize_stacktraces_for_grouping: Applies stack trace rules, adjusts frame in_app flags
  2. Server-side fingerprint rules: Can override default fingerprint
  3. Grouping algorithm (only runs when fingerprint is unset or contains {{ default }})

Platform-specific stack grouping strategies[R2]:

  • Python: Each frame contributes module + function + context-line (source line)
  • JavaScript: module + filename (basename lowercase) + context-line; function name omitted due to source map instability
  • Native: Primarily uses the cleaned function name

Frame Hiding: Frames from non-application code are marked “out of app,” but the full hash is still generated to support implicit merging[R2].

Session Replay Storage

Sentry Replay is built on the rrweb technology stack[R7]. Replay data is stored as segments in Blob/object storage, deeply integrated with the error system (replay can be linked via error_id). Optimization strategies include dynamic sampling and adaptive compression[R7].

High-Cardinality Handling

The Sentry engineering team notes that high-cardinality aggregation can crash performance through memory exhaustion[R6]. ClickHouse’s columnar storage treats high-cardinality fields (such as trace_id, user_id) as regular compressed columns, avoiding index bloat, with lightweight secondary indexes like bloom filters for point lookups[R6].

This is a core reason Sentry chose ClickHouse over TSDB — columnar storage is naturally high-cardinality-friendly, while traditional TSDB inverted indexes expand exponentially under high cardinality.

Datadog RUM

Hierarchical Event Model

Datadog RUM is built around a hierarchical event model[R3], where event types are organized in a tree structure with different retention policies:

mermaid
flowchart TD
    S[Session<br/>30-day retention] --> V[View<br/>30-day retention]
    V --> R[Resource<br/>15-day retention]
    V --> L[Long Task<br/>15-day retention]
    V --> E[Error<br/>30-day retention]
    V --> A[Action<br/>30-day retention]

    style S fill:#9C27B0,color:#fff
    style V fill:#2196F3,color:#fff
    style R fill:#4CAF50,color:#fff
    style L fill:#4CAF50,color:#fff
    style E fill:#FF9800,color:#fff
    style A fill:#FF9800,color:#fff

The diagram shows Datadog RUM’s event hierarchy: each Session contains multiple Views (page visits), and each View is associated with Resource, Long Task, Error, and Action sub-events. Session (purple) and View (blue) are retained for 30 days, Resource and Long Task (green) for 15 days, Error and Action (orange) for 30 days.

Event TypeRetentionDescription
Session30 daysUser session, unique session.id. Resets after 15 min inactivity, ends after 4 hours activity
View30 daysEach page visit, correlates all child resource/long-task/error/action
Resource15 daysImage/XHR/Fetch/CSS/JS loading with detailed timing
Long Task15 daysTasks blocking main thread > 50ms
Error30 daysAll frontend errors emitted by the browser
Action30 daysUser interactions (clicks, etc.), customizable

Hierarchy: Session → View → (Resource, Long Task, Error, Action), linked via session.id / view.id[R3].

Web Vitals Collection and Storage

View events directly carry Core Web Vitals fields[R3]:

FieldDescription
view.largest_contentful_paint (LCP)Largest Contentful Paint
view.interaction_to_next_paint (INP)Interaction to Next Paint
view.cumulative_layout_shift (CLS)Cumulative Layout Shift
view.first_contentful_paint (FCP)First Contentful Paint
view.first_input_delay (FID)First Input Delay
view.first_byte (TTFB)Time to First Byte

Resource events are based on the Performance Resource Timing API, collecting duration, size, connect.duration, ssl.duration, dns.duration, and more[R3].

URL Auto-Grouping: view.url_path_group normalizes /dashboard/123 and /dashboard/456 into /dashboard/?, effectively controlling URL dimension cardinality[R3].

Backend Storage Architecture

Datadog RUM’s backend storage employs a dual-engine strategy[R6][R9][R10]:

  • Husky: Datadog’s self-developed columnar storage engine. Events are stored as fragments in object storage (S3/GCS). Queries discover relevant fragments via metadata, dispatch them to a worker pool for parallel scanning, and merge results[R9]. This is essentially compute-storage separated columnar OLAP.
  • RTDB (Real-Time Database, 6th generation): Built in Rust with a Tokio-based async ingestion pipeline[R10].

Datadog’s design philosophy is wide-event columnar storage + compute-storage separation, rather than the traditional TSDB label-set model, to natively handle high-cardinality RUM data[R6][R9]. Husky handles long-term historical storage and offline queries, while RTDB manages real-time ingestion and near-real-time queries — a complementary pairing.

Grafana Faro

Positioning and Architecture

Grafana Faro is Grafana Labs’ open-source frontend observability Web SDK. Its core approach is reusing the existing Grafana LGTM stack (Loki for logs / Grafana for visualization / Tempo for tracing / Mimir for metrics) rather than building dedicated storage[R4].

Data flow[R4]:

  1. Faro Web SDK integrates into the application, collects data, and submits to the collector
  2. Collector receives SDK data and writes to Loki (logs/exceptions) and Tempo (traces) separately
  3. Grafana visualizes RUM data and correlates it with backend/infrastructure data

Storage Backend Characteristics

Faro introduces no new storage engine; it maps RUM signals onto existing LGTM components[R4][R8][R17]:

  • Loki: Indexes only labels, not log content, well-suited for structured JSON logs[R8]
  • Tempo: Microservices architecture; trace spans are sorted as Apache Parquet schema columns and written to object storage[R4][R17]
  • Mimir: Prometheus-compatible metrics storage, hosting Web Vitals metrics[R4]

Design Tradeoffs

Faro’s defining characteristic is not building dedicated time-series storage, but mapping RUM signals onto existing observability backends. The advantage is full-stack open-source correlation without additional storage component operations; the tradeoff is that the RUM event model is less natively unified than Datadog’s — frontend errors go to Loki as logs, performance data goes through Mimir’s metrics path, tracing data goes through Tempo, and cross-signal correlation relies on Grafana’s unified query layer.

General RUM Technical Patterns

Browser Data Collection

All mainstream RUM solutions share the same data source layer[R3][R14][R15]:

  • Performance API: Navigation Timing, Resource Timing, Paint Timing (FCP/LCP), Event Timing (INP/FID), LayoutShift (CLS) are the data sources for all RUM solutions[R3][R14]
  • Web Vitals library: Unified collection of LCP, INP, CLS, FCP, TTFB[R15]
  • Reporting channels: navigator.sendBeacon (reliable reporting on page unload), fetch/XHR (regular batch reporting)[R15]
mermaid
flowchart TD
    A[Browser<br/>Performance API] --> B[Web Vitals 库<br/>Web Vitals lib]
    B --> C[sendBeacon / fetch]
    C --> D[Collector]
    D --> E[Columnar OLAP<br/>not TSDB]

    style A fill:#2196F3,color:#fff
    style B fill:#2196F3,color:#fff
    style C fill:#FF9800,color:#fff
    style D fill:#FF9800,color:#fff
    style E fill:#4CAF50,color:#fff

The diagram shows the common RUM data path from browser to storage: the Browser Performance API provides raw performance data, the Web Vitals library (blue) standardizes collection, data is reported via sendBeacon or fetch (orange) to the Collector layer, and ultimately written to columnar OLAP rather than traditional TSDB. This paradigm has been adopted by all mainstream RUM solutions.

Web Vitals Measurement Timeline

Web Vitals metrics are measured at different stages of page load. Understanding their sequence is critical for diagnosing performance bottlenecks.

Web Vitals Page Load TimelinePage Loadt=0FCPFirst Contentful Paint~1.2sLCPLargest Contentful Paint~2.5sCLSCumulative Layout Shift~3.0sINPInteraction to Next Paint~4.2sFCP → LCP → CLS → INP: four metrics covering the full user experienceLCP and CLS together account for 50% of page load experience weight

Animation description: The animation above progressively reveals Web Vitals core metrics along a page load timeline. FCP (blue) appears first when the browser renders any content; LCP (orange) fires when the largest content element renders; CLS (red) continuously monitors layout shifts throughout the page lifecycle; the user’s first interaction triggers INP (purple) measurement. Understanding when each metric is measured helps accurately diagnose performance issues.

Why Not TSDB

RUM data is inherently event streams with massive high-cardinality attributes, not pure numeric time series. Traditional TSDB label-set models suffer “tag explosion” under high cardinality — adding a single user_id label with millions of users creates millions of independent series[R6].

Consequently, mainstream RUM backends do not reuse pure TSDB; they adopt columnar OLAP for “wide events”:

  • Columnar storage + primary key sorting → 10-30x compression
  • High-cardinality fields are just regular compressed columns, causing no index bloat
  • Vectorized execution + multi-core parallelism → sub-second aggregation

This is the shared choice of Sentry (Snuba/ClickHouse), Datadog (Husky), and Grafana Faro (Loki+Tempo)[R5][R6][R9].

Sampling and Reporting Strategies

To control data volume and cost, all solutions implement layered sampling strategies[R7][R8][R15]:

  • Session-level sampling (not event-level): Ensures all events in a session are either all sampled or all dropped[R15]
  • Dynamic sampling: Sentry supports fixed-ratio, rule-based, and dynamic sampling[R7]
  • Rate limiting: Token bucket rate limiting at the Collector layer[R8]

Session-level sampling is the gold standard practice in RUM: with event-level sampling, a view within a session might be sampled while its resources are dropped, creating “broken” analysis that cannot reconstruct the full user journey.

References

[R1] [Official] Sentry Self-hosted Data Flow: https://develop.sentry.dev/self-hosted/data-flow/

[R2] [Official] Sentry Grouping: https://develop.sentry.dev/grouping/

[R3] [Official] Datadog RUM Browser Data Collected: https://docs.datadoghq.com/real_user_monitoring/browser/data_collected/

[R4] [Official] What is Grafana Faro?: https://grafana.com/oss/faro/

[R5] [Official] Introducing Snuba: Sentry’s New Search Infrastructure: https://blog.sentry.io/introducing-snuba-sentrys-new-search-infrastructure/

[R6] [Reference] The high-cardinality trap (ClickHouse): https://clickhouse.com/resources/engineering/high-cardinality-slow-observability-challenge

[R7] [Reference] Sentry Advanced Features: Performance Analysis and UX Monitoring (Chinese): https://blog.csdn.net/gitblog_00554/article/details/150620759

[R8] [Official] Grafana Agent app_agent_receiver_config: https://grafana.com/docs/agent/latest/configuration/integrations/integrations-next/app-agent-receiver-config/

[R9] [Official] Husky: Efficient compaction at Datadog scale: https://www.datadoghq.com/blog/engineering/husky-storage-compaction/

[R10] [Reference] Datadog 6th Generation Time-Series Database (Chinese): https://juejin.cn/post/7555079970490531876

[R14] [Official] Cloudflare RUM beacon: https://developers.cloudflare.com/speed/observatory/rum-beacon/

[R15] [Reference] Real User Monitoring (RUM) with web-vitals Library: https://webperfclinic.com/article/real-user-monitoring-web-vitals-library-2026-setup-guide

[R17] [Official] Grafana Tempo architecture: https://grafana.com/docs/tempo/next/introduction/architecture/