01

Requirements & scope

Problem Statement & Requirements

Design a large-scale metrics collection, storage, alerting, and visualization platform that ingests millions of time-series data points per second from infrastructure and applications, stores them efficiently, and powers real-time dashboards and alerts — similar to Datadog, Prometheus + Grafana, New Relic, or Splunk.

Functional Requirements

  • Metrics ingestion — collect counters, gauges, histograms, and summaries from agents, SDKs, and push APIs
  • Time-series storage — store metric data points (timestamp, value, tags) with configurable retention
  • Querying — flexible query language for aggregation (sum, avg, p99, rate), grouping (by host, service, region), and time-range selection
  • Dashboards — build and share real-time dashboards with charts, tables, heatmaps, and topN lists
  • Alerting — define threshold, anomaly, and composite alerts; route notifications (PagerDuty, Slack, email)
  • Tagging / Dimensions — every metric has tags (key:value) for flexible slicing (e.g., http.requests{service=api,status=200,region=us-east})
  • Downsampling / Rollups — automatically aggregate older data to coarser granularity (1s → 1m → 1h) to save storage
  • Anomaly detection — ML-based detection of unexpected patterns (spike, drop, trend shift)
  • Service-level objectives (SLOs) — define and track SLIs/SLOs with error-budget burn-rate alerts

Non-Functional Requirements

  • High write throughput — ingest 10M+ data points per second
  • Low query latency — dashboard queries return in < 500 ms even over weeks of data
  • High availability — 99.99% for ingestion (if monitoring goes down, you're flying blind)
  • Durability — metrics data must not be lost once acknowledged
  • Horizontal scalability — scale ingestion and storage independently
  • Retention — configurable: raw data 15 days, 1-min rollups 3 months, 1-hour rollups 2 years
  • Multi-tenancy — support thousands of customers with isolation and per-tenant rate limits

Out of Scope

  • Log aggregation (ELK/Splunk detailed design)
  • Distributed tracing (Jaeger/Zipkin)
  • APM (Application Performance Monitoring) code-level profiling
  • Synthetic monitoring (external probing)
02

Scale estimations

Scale Estimations

Traffic

MetricValue
Active metric time-series (unique tag combinations)1B
Data points ingested / second10M
Data points ingested / day10M × 86,400 = ~864B
Query requests / second (dashboards + alerts)50K RPS
Active dashboards (refreshing)500K
Active alert rules5M
Alert evaluations / second5M rules / 60s cadence = ~83K evaluations/sec
Customers (multi-tenant)50K

Storage

MetricValue
Raw data point size16 bytes (8B timestamp + 8B float64 value)
Tags per data pointavg 100 bytes (stored as tag-set hash → 8 bytes reference)
Effective per-point storage (with compression)~2-4 bytes (Gorilla compression: 1.37 bytes/point avg)
Raw data (15 days)864B/day × 15 × 2 bytes = ~26 TB
1-min rollups (3 months)864B / 60 × 90 × 4 bytes = ~5.2 TB
1-hour rollups (2 years)864B / 3600 × 730 × 8 bytes = ~1.4 TB
Tag metadata index1B series × 200 bytes = ~200 GB
Total storage~33 TB (with compression — remarkably compact)

Bandwidth

MetricValue
Ingestion (10M pts/s × 24 bytes avg including tags)~240 MB/s
Query responses (50K/s × 10 KB avg)~500 MB/s
Internal replication~480 MB/s (2× ingestion)

Hardware Estimate

ComponentSpec
Ingestion gateways20-50 (stateless, auto-scaling)
Kafka (ingestion buffer)10-20 brokers
Time-series storage nodes50-100 (SSD-backed)
Query / aggregation nodes30-50 (CPU + RAM heavy)
Tag index (inverted index)10-20 nodes
Alert evaluation workers20-50
Redis (alert state, recent data)10-20 nodes
03

Layered architecture

Phase 1 of 4 · Client & edge

Start with the entry points

First, define who uses the system and where requests enter. This is the boundary of the design, not yet a complete request path.

Agents + SDKs
Collect timestamped metrics with controlled label cardinality and bounded local buffers.
Ingestion gateway
Authenticate writers, normalize samples, and reject cardinality explosions early.
Architecture after phase 1. Includes Agents + SDKs, Ingestion gateway. Agents + SDKs connects to Ingestion gateway.
Phase 1 — Start with Agents + SDKs, Ingestion gateway.SVGSVGEditable diagramEditable diagram

This first layer defines the system boundary. The next chapters add application logic and data dependencies. After examining the request paths and tradeoffs, we will add operational safeguards and bring the complete architecture together.

04

API & contracts

Phase 2 of 4 · Application & compute

Add the application logic

Keep the entry points and add the services that do the work. Give each service a clear responsibility before defining its contracts. Storage and background dependencies come next.

Ingestion workers
Turn label sets into series identities and write samples efficiently in batches.
Query + alert engine
Query recent windows and evaluate alert rules with explicit handling for missing data.
Architecture after phase 2. Includes Agents + SDKs, Ingestion gateway, Ingestion workers, Query + alert engine. Agents + SDKs connects to Ingestion gateway.
Phase 2 — Added Ingestion workers, Query + alert engine. Earlier layers remain in place.SVGSVGEditable diagramEditable diagram

API Design

Submit Metrics (Push API)

EXAMPLE
POST /api/v1/series
Content-Type: application/json
DD-API-KEY: <api_key>

Request:
{
  "series": [
    {
      "metric": "http.requests",
      "type": "count",
      "interval": 10,
      "points": [[1712150400, 1523], [1712150410, 1487]],
      "tags": ["service:api", "status:200", "region:us-east"],
      "host": "web-server-42"
    },
    {
      "metric": "system.cpu.user",
      "type": "gauge",
      "points": [[1712150400, 72.5]],
      "tags": ["host:web-server-42", "env:production"],
      "host": "web-server-42"
    }
  ]
}

Response (202 Accepted):
{
  "status": "ok",
  "points_accepted": 3
}

Query Metrics

EXAMPLE
POST /api/v1/query
Authorization: Bearer <token>

Request:
{
  "query": "avg:http.latency{service=api,region=us-east} by {status_code}",
  "from": 1712146800,    // epoch seconds
  "to": 1712150400,      // last 1 hour
  "rollup": {
    "method": "avg",
    "interval": 60        // 1-minute buckets
  }
}

Response (200 OK):
{
  "series": [
    {
      "metric": "http.latency",
      "tags": {"service": "api", "region": "us-east", "status_code": "200"},
      "pointlist": [
        [1712146800, 23.4],
        [1712146860, 25.1],
        [1712146920, 22.8],
        ...
      ],
      "unit": "milliseconds",
      "query_index": 0
    },
    {
      "metric": "http.latency",
      "tags": {"service": "api", "region": "us-east", "status_code": "500"},
      "pointlist": [[1712146800, 187.3], ...],
      "unit": "milliseconds",
      "query_index": 0
    }
  ],
  "query_time_ms": 87
}

Create Alert

EXAMPLE
POST /api/v1/monitors
Authorization: Bearer <token>

Request:
{
  "name": "High API Error Rate",
  "type": "metric_alert",
  "query": "avg(last_5m):sum:http.errors{service=api} / sum:http.requests{service=api} > 0.05",
  "message": "@pagerduty-oncall API error rate is above 5%! Check {{host}}.",
  "tags": ["team:platform", "severity:critical"],
  "options": {
    "thresholds": {
      "critical": 0.05,
      "warning": 0.02
    },
    "notify_no_data": true,
    "no_data_timeframe": 10,
    "evaluation_delay": 60,
    "renotify_interval": 300
  }
}

Response (201 Created):
{
  "monitor_id": 12345,
  "name": "High API Error Rate",
  "status": "OK",
  "created_at": "2026-04-03T10:00:00Z"
}

Create Dashboard

EXAMPLE
POST /api/v1/dashboards
Authorization: Bearer <token>

Request:
{
  "title": "API Health Dashboard",
  "widgets": [
    {
      "type": "timeseries",
      "title": "Request Rate",
      "queries": [
        {"query": "sum:http.requests{service=api} by {status_code}.as_rate()", "display_type": "bars"}
      ],
      "time": {"live_span": "1h"}
    },
    {
      "type": "query_value",
      "title": "p99 Latency",
      "queries": [
        {"query": "p99:http.latency{service=api}", "aggregator": "last"}
      ],
      "conditional_formats": [
        {"comparator": ">", "value": 500, "palette": "red"},
        {"comparator": ">", "value": 200, "palette": "yellow"}
      ]
    },
    {
      "type": "toplist",
      "title": "Top Error Endpoints",
      "queries": [
        {"query": "sum:http.errors{service=api} by {endpoint}.as_count()"}
      ],
      "limit": 10
    }
  ]
}
05

Data model

Phase 3 of 4 · Data & async work

Connect data and background work

The services now need their dependencies: durable state, fast lookups, or work that continues outside a request. Separate these responsibilities rather than treating every component as a database.

Stream buffer
Absorb ingestion spikes and replay after storage incidents without blocking collection indefinitely.
Time-series database
Optimize for sequential samples, retention windows, and range queries.
Rollup storage
Keep high-resolution recent data and coarser history with declared aggregation semantics.
Architecture after phase 3. Includes Agents + SDKs, Ingestion gateway, Ingestion workers, Query + alert engine, Stream buffer, Time-series database, Rollup storage. Agents + SDKs connects to Ingestion gateway. Ingestion gateway connects to Stream buffer. Stream buffer connects to Ingestion workers. Ingestion workers connects to Time-series database. Query + alert engine connects to Time-series database. Time-series database connects to Rollup storage.
Phase 3 — Added Stream buffer, Time-series database, Rollup storage. Earlier layers remain in place.SVGSVGEditable diagramEditable diagram

Data Model

Time-Series Data Point

Time-Series Data Point
Time-Series Data PointSVGSVGEditable diagramEditable diagram

On-Disk Storage Layout (Time-Partitioned Blocks)

On-Disk Storage Layout (Time-Partitioned Blocks)
On-Disk Storage Layout (Time-Partitioned Blocks)SVGSVGEditable diagramEditable diagram

Gorilla Compression (Core Innovation)

Gorilla Compression (Core Innovation)
Gorilla Compression (Core Innovation)SVGSVGEditable diagramEditable diagram

Tag Inverted Index

Tag Inverted Index
Tag Inverted IndexSVGSVGEditable diagramEditable diagram
06

Core design decisions

Core Design Decisions

Decision 1: Push vs Pull Collection Model

Decision 1: Push vs Pull Collection Model
Decision 1: Push vs Pull Collection ModelSVGSVGEditable diagramEditable diagram

Decision 2: Time-Series Storage Engine

Decision 2: Time-Series Storage Engine
Decision 2: Time-Series Storage EngineSVGSVGEditable diagramEditable diagram

Decision 3: Alert Evaluation Architecture

Decision 3: Alert Evaluation Architecture
Decision 3: Alert Evaluation ArchitectureSVGSVGEditable diagramEditable diagram
07

Request flows

Detailed Flow Diagrams

Metrics Ingestion Flow

Metrics Ingestion Flow
Metrics Ingestion FlowWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram

Dashboard Query Flow

Dashboard Query Flow
Dashboard Query FlowWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram

Alert Evaluation + Notification Flow

Alert Evaluation + Notification Flow
Alert Evaluation + Notification FlowWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram
08

Performance & caching

Downsampling & Retention

Downsampling & Retention
Downsampling & RetentionSVGSVGEditable diagramEditable diagram
09

Advanced design

Multi-Tenancy

Multi-Tenancy
Multi-TenancySVGSVGEditable diagramEditable diagram
10

Edge cases

Handling Edge Cases

High-Cardinality Tags

EXAMPLE
Problem: Metric http.requests tagged with request_id (unique per request)
  → Billions of unique series → unbounded storage growth
  → Queries become impossibly slow (scan billions of series)

Detection:
  Monitor: unique_series_count per (tenant, metric_name)
  If growth rate > 10K new series/hour → alert

Prevention:
  1. Client-side: agent rejects tags with cardinality > threshold
  2. Server-side: ingestion worker checks new series rate
     → If > 100K unique series for one metric → drop new series
     → Return warning header to agent
  3. Documentation: educate users about low-cardinality tags only
     ✅ service, host, region, status_code, endpoint
     ❌ request_id, user_id, session_id, trace_id

Late-Arriving Data Points

EXAMPLE
Problem: Agent buffers during network outage → sends 30 min of old data at once

Impact:
  • Data points arrive for already-flushed blocks
  • Alert evaluations may need recalculation

Solution:
  • Accept late data up to a configurable window (e.g., 1 hour)
  • Write to a "late-arrival" WAL segment
  • Merge into existing blocks during next compaction
  • For alerts: re-evaluate sliding windows when late data arrives
  • Reject data older than 1 hour (return 400 with "too old" error)

Clock Skew Across Agents

EXAMPLE
Problem: Different hosts have slightly different clocks
  → Data points at "same time" have different timestamps
  → Aggregation across hosts produces incorrect results

Solution:
  1. NTP synchronization on all hosts (< 100ms drift acceptable)
  2. Ingestion gateway records server-side receive timestamp
  3. If |agent_timestamp - server_timestamp| > 5 minutes:
     → Override with server timestamp
     → Log warning to agent
  4. Queries tolerate minor drift via configurable alignment window

Alert Flapping

EXAMPLE
Problem: Metric oscillates around threshold → alert fires/recovers every minute
  → Oncall engineer gets 30 notifications in 30 minutes

Solution: Hysteresis + evaluation window

  Alert: http.error_rate > 5% for 5 minutes
  Recovery: http.error_rate < 3% for 5 minutes   ← different threshold!

  The gap (3% to 5%) is the hysteresis band.
  Must go BELOW 3% to recover, not just below 5%.

  Additional measures:
  • Renotify interval: minimum 5 minutes between notifications
  • Auto-mute if alert fires > 10 times in 1 hour → likely flapping
  • Dashboard flag: "This alert is flapping — consider adjusting threshold"
11

Tradeoffs

Tradeoffs & Design Decisions Summary

DecisionOption AOption BChosenWhy
CollectionPull (Prometheus)Push (agent-based)Both (push primary, pull adapter)Push for multi-tenant SaaS (firewall-friendly); pull adapter for Prometheus compat
Storage engineGeneral-purpose DBPurpose-built TSDBTSDB with Gorilla compression11.7× compression; append-only optimized; time-partitioned blocks for retention
CompressionLZ4/SnappyGorilla (delta-of-delta + XOR)GorillaPurpose-built for time-series: 1.37 bytes/point vs 16 bytes raw
Tag indexB-tree indexRoaring BitmapsRoaring BitmapsCompact, fast set intersection for multi-tag queries; handles 1B series
Ingestion bufferDirect to TSDBKafka → TSDBKafka bufferDecouples collection from storage; absorbs bursts; enables replay on failure
Alert evaluationPull (query TSDB per rule)Push (streaming, route to relevant rules)Push/streaming83K evals/sec via pull = TSDB overload; streaming routes only to affected rules
Rollup storageSame store, flaggedSeparate storage tiersSeparate tiersDifferent access patterns: raw is write-heavy, rollups are read-heavy
Time partitioningFixed-size blocksTime-range blocks (2h)Time-range (2h)Self-contained blocks; easy deletion for retention; efficient compaction
Query executionSingle-nodeScatter-gather across TSDB nodesScatter-gatherParallel fanout across nodes holding relevant series; gather for final aggregation
Multi-tenancySeparate DB per tenantShared cluster with isolationShared + isolationCost-effective at 50K tenants; per-tenant rate limits + quotas prevent noisy neighbor
12

Reliability & fault tolerance

Phase 4 of 4 · Operations & resilience

Add operational safeguards

With the request paths and tradeoffs established, add the controls that help detect trouble and keep the system operating. The final diagram retains every layer introduced so far.

Dashboards + paging
Separate symptom-based paging from diagnostic dashboards and monitor the monitoring system itself.
Architecture after phase 4. Includes Agents + SDKs, Ingestion gateway, Ingestion workers, Query + alert engine, Stream buffer, Time-series database, Rollup storage, Dashboards + paging. Agents + SDKs connects to Ingestion gateway. Ingestion gateway connects to Stream buffer. Stream buffer connects to Ingestion workers. Ingestion workers connects to Time-series database. Query + alert engine connects to Time-series database. Time-series database connects to Rollup storage. Query + alert engine connects to Dashboards + paging.
Phase 4 — Added Dashboards + paging. Earlier layers remain in place.SVGSVGEditable diagramEditable diagram

Reliability & Fault Tolerance

Single Points of Failure & Mitigations

Single Points of Failure & Mitigations
Single Points of Failure & MitigationsSVGSVGEditable diagramEditable diagram

Graceful Degradation

EXAMPLE
Tier 1 (single TSDB node down):
  → Queries return partial results (other nodes still respond)
  → Warning banner: "Some data may be incomplete"
  → Writes to that shard go to replica

Tier 2 (Kafka down):
  → Agents buffer locally (1 hour of data in memory/disk)
  → Alert evaluations continue on existing recent data
  → When Kafka recovers: agents flush buffered data (late arrival)

Tier 3 (Tag index down):
  → Cannot resolve tag queries (which series match "service=api"?)
  → Fallback: if query specifies exact series_id → still works
  → Dashboard shows "Tag resolution degraded"
  → Rebuild index from TSDB metadata (~minutes)

Tier 4 (Alert engine down):
  → NO ALERTS FIRING — this is the worst failure mode
  → Must be highest priority to recover
  → Mitigation: dead-man's switch alert
    → External service expects heartbeat every 5 min
    → If no heartbeat → alert via independent channel
    → "Alert engine is down" — monitors the monitors
13

Production architecture

High-Level Architecture

High-Level Architecture
High-Level ArchitectureSVGSVGEditable diagramEditable diagram

Component Breakdown

Component Breakdown
Component BreakdownSVGSVGEditable diagramEditable diagram

Full System Architecture (Production-Grade)

Full System Architecture (Production-Grade)
Full System Architecture (Production-Grade)SVGSVGEditable diagramEditable diagram
14

Further exploration

SLO Monitoring (Bonus Deep Dive)

SLO Monitoring (Bonus Deep Dive)
SLO Monitoring (Bonus Deep Dive)SVGSVGEditable diagramEditable diagram
15

Interview playbook

Interview Tips

  1. Start with the write path — 10M points/sec — "Time-series is a write-dominated workload." Explain: agents batch + compress → push to ingestion gateway → Kafka buffer → ingestion workers → TSDB with WAL. Kafka decouples collection from storage and absorbs bursts.

  2. Gorilla compression is the key insight — Don't just say "compress data." Explain: delta-of-delta for timestamps (1 bit for regular intervals), XOR for values (encode only changed bits). 1.37 bytes/point average = 11.7× compression. This is why a monitoring system storing 864B points/day needs only ~26 TB.

  3. Time-partitioned blocks — 2-hour immutable blocks. Active block in memory (WAL for durability). Old blocks compressed, indexed, read-optimized on SSD. Deletion = drop a block file (instant, no GC). This is the Prometheus TSDB model.

  4. Tag inverted index with Roaring Bitmaps — Query service=api AND region=us-east resolves to: bitmap A ∩ bitmap B = matching series IDs. Roaring Bitmaps are compact (~200 GB for 1B series) and support fast set operations. This is how tag-based queries stay fast.

  5. Rollup/downsampling is critical for long-term queries — Raw 10s data retained 15 days, 1-min rollups 3 months, 1-hour rollups 2 years. Query engine auto-selects the right tier based on time range. Transparent to the user. This is how a dashboard over "last 6 months" returns in < 500 ms.

  6. Alert evaluation is streaming, not polling — 5M rules evaluated every 60s = 83K evals/sec. If each queries TSDB = overload. Instead: as data points arrive, route to matching alert rules (inverted index: metric+tags → rule_ids). Most data matches zero rules. Only evaluate relevant rules.

  7. Multi-tenancy is about isolation — Per-tenant rate limits, series quotas, query timeouts. Cardinality explosion protection: if a tenant's metric has > 100K unique tag combinations, reject new series. One bad tenant must not affect others.

  8. High-cardinality tags are the #1 operational issue — request_id, user_id, trace_id as tags = billions of unique series = storage explosion + slow queries. Detect + prevent at ingestion. This is the most common real-world problem in monitoring systems.

  9. Meta-monitoring — who monitors the monitors? — Dead-man's switch: external service expects heartbeat every 5 min. If missing → alert via independent channel (SMS, not the monitoring system itself). If your monitoring is down, you're flying blind.

  10. SLO burn-rate alerting shows depth — Don't just describe threshold alerts. Explain: SLI as a time-series, error budget calculation, burn rate = actual/budgeted error rate, multi-window alerting (fast burn pages, slow burn creates ticket). This is the Google SRE Book approach and demonstrates production maturity.