Compile Ready
All system design questions
System Design/Foundations

Metrics Collection

Design a time-series metrics system that collects, aggregates, and stores millions of data points per second.

Intermediate 50m interview 22m read Medium frequency Popularity 78
Amazon Microsoft Databricks

Problem Statement

Design a Metrics Collection platform like Prometheus, Datadog Metrics, or a cloud monitoring time-series backend. Applications, hosts, containers, and infrastructure components emit counters, gauges, histograms, and summaries. The system must ingest samples continuously, store them efficiently as time series, and answer aggregation queries over recent and historical windows.

At interview scale, assume millions of active series, hundreds of thousands of samples per second, high-cardinality labels, and many teams querying dashboards and alerts at the same time. The hard part is not accepting one metric point; it is controlling cardinality, sharding series safely, compressing time-series data, keeping writes durable, and making PromQL-like queries fast enough without scanning the world.

The default design should support both pull collection, where scrapers discover targets and periodically read metrics endpoints, and push collection, where agents or StatsD-style clients send batched metrics. It should keep raw recent data for detailed debugging, downsample older data for long retention, and isolate ingestion from query spikes.

Business use case

Metrics are the operating dashboard for modern software. They power service health dashboards, SLO tracking, capacity planning, autoscaling signals, incident detection, and release validation. Teams need to ask questions such as whether checkout error rate increased after a deploy, which region is saturated, or whether database latency is breaching an objective.

Businesses also use metrics to reduce downtime and infrastructure cost. A strong metrics platform makes production behavior visible, detects regressions quickly, and gives engineering leaders reliable signals for reliability investments.

Functional Requirements

  • Ingest counter, gauge, histogram, and summary metrics from applications, agents, and infrastructure.

  • Support pull-based scraping of registered targets and push-based ingestion for agents, StatsD, and remote write clients.

  • Attach labels or dimensions to metrics and identify unique time series from metric name plus label set.

  • Store raw samples for recent retention and downsampled rollups for long-term retention.

  • Provide instant and range queries with filtering, grouping, aggregation, rate calculations, and percentile-style histogram queries.

  • Expose metadata APIs for metric names, label values, active series, targets, and ingestion health.

  • Support alert evaluation over metric expressions without blocking ingestion.

  • Enforce per-tenant quotas, label allowlists, and cardinality limits.

Non-Functional Requirements

Ingestion throughput

The platform should sustain hundreds of thousands of samples per second on average and scale to higher peaks by partitioning series across ingestion shards. Writes must be batched, append-oriented, and protected by a durable log or write-ahead log.

Query latency

Common dashboard queries over the last hour should complete in under 1 second at p95. Larger ad hoc queries over days or months can take several seconds, but they need limits, cancellation, and progressive execution so they do not starve ingestion or other users.

Availability

Ingestion should remain available during query outages, rollup delays, and partial storage failures. Recent data loss should be extremely rare because metrics are used during incidents, but a short query freshness gap is usually better than dropping writes.

Durability and retention

Recent raw samples should survive node restarts and single-zone failures. Retention should be tiered, such as 15 days of raw samples, 180 days of 5-minute rollups, and 2 years of hourly rollups.

Cardinality control

The system must detect and limit label combinations that create unbounded series, such as user_id, request_id, or full URL path. Cardinality protection is a first-class requirement because one bad deployment can multiply storage, indexes, and query cost.

Multi-tenancy

Tenants need isolated quotas, retention policies, access control, and noisy-neighbor protection. A single customer or team should not be able to exhaust global ingestion, index, or query capacity.

Cost efficiency

Samples are tiny but extremely numerous. The design should use delta-of-delta timestamp compression, XOR value compression, chunked storage, compaction, downsampling, and cold object storage for older blocks.

Capacity Estimation

Assumptions

Assume 3M active time series on average, 5M active series at peak, a 15-second default scrape interval, and 30 days per month. A raw sample has an 8-byte timestamp and an 8-byte float value before compression. Labels are stored once per series in the index and metadata store rather than repeated on every sample.

Assume compressed TSDB chunks average 2 to 4 bytes per sample with delta-of-delta and XOR encoding. Use 3 replicas for recent data, 15 days of raw retention, 180 days of 5-minute rollups, and 2 years of 1-hour rollups. Query traffic is assumed to be 1,000 range queries per second at peak across dashboards, alerts, and ad hoc users.

Active series

3M average, 5M peak

Each unique metric name plus label set is one series

Average ingest rate

200,000 samples per second

3M active series divided by 15 seconds

Peak ingest rate

500,000 samples per second

Peak series count plus shorter scrape intervals and bursts

Daily raw samples

17.3B samples per day

200,000 samples per second times 86,400 seconds

Raw sample bytes

276 GB per day

17.3B samples times 16 bytes before compression and metadata

Compressed chunks

35 to 70 GB per day

2 to 4 bytes per sample before replication and indexes

Recent raw retention

1.0 to 1.5 TB compressed

15 days of compressed chunks before replication and index overhead

Recent replicated footprint

5 to 8 TB

3 replicas plus index, WAL, compaction, and headroom

5-minute rollup samples

864M samples per day

3M series times 288 five-minute buckets, before dropping unused dimensions

Writer shards

128 to 256 shards

At 500,000 peak samples per second, each shard handles roughly 2,000 to 4,000 samples per second

Calculations

  • Average ingest: 3M active series divided by a 15-second scrape interval is 200,000 samples per second.
  • Daily samples: 200,000 samples per second times 86,400 seconds is 17.28B samples per day.
  • Raw bytes: 17.28B samples times 16 bytes for timestamp plus value is about 276 GB per day, before labels, WAL, replication, and indexes.
  • Compression: at 2 to 4 bytes per sample, daily compressed chunks are about 35 to 70 GB. This assumes stable scrape intervals, similar neighboring values, and chunk encoding with delta-of-delta timestamps and XOR values.
  • Raw retention: 15 days times 35 to 70 GB is about 525 GB to 1.05 TB of compressed chunks. Add indexes, metadata, WAL, compaction workspace, and safety headroom to plan around 1.0 to 1.5 TB before replication.
  • Replication: 3 replicas plus indexes and write amplification make recent storage roughly 5 to 8 TB.
  • Rollups: 5-minute buckets create 288 points per series per day. For 3M series, that is 864M rollup points per day before label reduction. Dropping high-cardinality dimensions in rollups is essential.
  • Sharding: at 500,000 peak samples per second, 128 shards receive about 3,900 samples per second each on average. 256 shards reduce per-shard load and provide room for hot series and tenant skew.

API Design

POST/api/v1/remote-write

Accepts batched metric samples from agents, sidecars, or compatible remote write clients. The ingestion gateway validates tenant identity, metric names, labels, timestamps, and payload size before appending to the ingestion log.

Request


{
  "tenantId": "team_checkout",
  "samples": [
    {
      "metric": "http_requests_total",
      "type": "counter",
      "labels": {
        "service": "checkout",
        "region": "us-east-1",
        "status": "500"
      },
      "timestamp": "2026-07-26T06:59:00Z",
      "value": 12843
    }
  ]
}

Response


{
  "acceptedSamples": 1,
  "rejectedSamples": 0,
  "nextAllowedAt": null
}
  • 202Batch accepted for durable ingestion
  • 400Invalid metric name, type, labels, timestamp, or value
  • 401Missing or invalid tenant credentials
  • 413Payload too large
  • 429Tenant ingest quota exceeded
GET/api/v1/targets

Returns scrape targets assigned to a scraper or agent. Service discovery can be backed by Kubernetes, Consul, cloud APIs, or static configuration.

Response


{
  "scraperId": "scraper_us_east_1_42",
  "targets": [
    {
      "targetId": "checkout_pod_17",
      "url": "https://10.24.9.17:9100/metrics",
      "intervalSeconds": 15,
      "labels": {
        "service": "checkout",
        "cluster": "prod-use1"
      }
    }
  ]
}
  • 200Targets returned
  • 401Scraper not authenticated
  • 403Scraper not allowed for tenant or region
POST/api/v1/targets/{targetId}/scrape-result

Allows scrape managers to report scrape health, sample counts, and failures. The control plane uses this for target health, missing metrics, and alerting on collection gaps.

Request


{
  "targetId": "checkout_pod_17",
  "scrapedAt": "2026-07-26T06:59:00Z",
  "durationMs": 82,
  "samples": 1432,
  "status": "success"
}

Response


{
  "recorded": true
}
  • 202Scrape result recorded
  • 400Invalid scrape result
  • 404Unknown target
GET/api/v1/query

Runs an instant PromQL-like query at a single timestamp. It supports selectors, arithmetic, aggregations, and functions such as rate over a recent window.

Request


query=sum(rate(http_requests_total[5m])) by (service)
time=2026-07-26T07:00:00Z
tenantId=team_checkout

Response


{
  "status": "success",
  "data": [
    {
      "metric": {
        "service": "checkout"
      },
      "value": [1785058800, 231.7]
    }
  ]
}
  • 200Query completed
  • 400Invalid query expression
  • 401Authentication required
  • 422Query exceeds series, time range, or cost limits
  • 503Query service temporarily unavailable
GET/api/v1/query_range

Runs a range query for dashboards and graph panels. The query frontend chooses raw chunks or rollups based on range and step size.

Request


query=histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))
start=2026-07-26T06:00:00Z
end=2026-07-26T07:00:00Z
step=30s

Response


{
  "status": "success",
  "resolution": "raw",
  "seriesReturned": 12,
  "data": [
    {
      "metric": {
        "service": "checkout"
      },
      "values": [
        [1785055200, 0.182],
        [1785055230, 0.191]
      ]
    }
  ]
}
  • 200Range query completed
  • 400Invalid query or time range
  • 401Authentication required
  • 422Query cost limit exceeded
  • 504Query timed out
GET/api/v1/labels/{labelName}/values

Returns known values for a label within a tenant and optional time range. This powers dashboard dropdowns but must be protected because high-cardinality labels can produce huge responses.

Request


tenantId=team_checkout
metric=http_requests_total
start=2026-07-26T06:00:00Z
end=2026-07-26T07:00:00Z

Response


{
  "labelName": "region",
  "values": ["us-east-1", "us-west-2", "eu-west-1"]
}
  • 200Label values returned
  • 401Authentication required
  • 422Response would exceed configured cardinality limits

Separate ingest APIs from query APIs. Ingestion should acknowledge only after a durable log append or local WAL persistence, while queries should be cost-limited and cancelable. Pull scraping is usually driven by internal scrape managers, but the samples should enter the same ingestion path as pushed samples after normalization.

Database Design

A metrics platform is logically a time-series database rather than a traditional relational application. The core identity is tenant_id plus metric_name plus normalized label set, which maps to a stable series_id. Samples for the same series are appended into compressed chunks ordered by time.

Use a relational-style schema to describe the entities, then implement the hot path in a distributed TSDB with an inverted index, append-only chunks, object storage blocks, and compacted rollups.

metric_series
tenant_idvarchar(128)Tenant or workspace that owns the series
series_iduint64Stable hash or allocated id for metric name plus labels
metric_namevarchar(255)Metric name such as http_requests_total
metric_typevarchar(32)counter, gauge, histogram, or summary
label_set_hashuint64Hash of sorted labels for idempotent series lookup
labels_jsonjsonCanonical sorted labels stored once per series
created_attimestampFirst seen time
last_seen_attimestampUsed to expire inactive series and estimate active cardinality
retention_policyvarchar(64)Raw and rollup retention class
tsdb_chunks
tenant_idvarchar(128)Tenant partition
shard_idintWriter shard owning the series for this time range
series_iduint64Series whose samples are encoded in the chunk
chunk_start_tstimestampChunk lower time bound
chunk_end_tstimestampChunk upper time bound
sample_countintNumber of samples in the chunk
encodingvarchar(32)Delta-of-delta timestamps and XOR values
encoded_bytesblobCompressed sample payload
checksumuint64Detects corruption during reads and compaction
label_index
tenant_idvarchar(128)Tenant partition
label_namevarchar(255)Label key such as service or region
label_valuevarchar(1024)Label value with size limits
block_idvarchar(128)Time block or index segment
series_postingsblobCompressed sorted list of series ids matching the label pair
updated_attimestampIndex freshness time
rollup_blocks
tenant_idvarchar(128)Tenant partition
rollup_idvarchar(128)Aggregate series or reduced-dimension identity
resolutionvarchar(32)5m, 1h, or another retention tier
bucket_start_tstimestampRollup bucket start
countdoubleCount of samples or events in bucket
sumdoubleSum for averages and rates
mindoubleMinimum observed value
maxdoubleMaximum observed value
histogram_bucketsjson nullableOptional aggregated histogram buckets

Indexes

  • metric_series.tenant_id, label_set_hash supports idempotent series creation and lookup during ingestion.
  • metric_series.tenant_id, metric_name supports metric discovery and query planning.
  • label_index.tenant_id, label_name, label_value, block_id is the inverted index used to resolve label matchers into series ids.
  • tsdb_chunks.tenant_id, shard_id, series_id, chunk_start_ts supports range scans for one series on the owning shard.
  • rollup_blocks.tenant_id, rollup_id, resolution, bucket_start_ts supports long-range queries without reading raw chunks.

Relationships

Each metric series has many compressed chunks. Label index entries point to sets of series ids, not to individual samples. Rollup blocks can be derived from raw chunks and may intentionally drop high-cardinality labels. Scrape targets and ingestion clients produce samples, but the durable storage model is organized around series and time.

NoSQL alternatives

At production scale, use a purpose-built distributed TSDB rather than a generic row store for samples. The hot write path resembles Kafka or a partitioned log feeding shard-local TSDB writers. Recent chunks can live on fast SSDs, compacted immutable blocks can move to object storage, and metadata can live in a strongly consistent key-value store.

Cassandra, Bigtable, or DynamoDB can store time-bucketed samples for a simpler design, but they often waste space and struggle with PromQL-style label matching unless paired with an inverted index. ClickHouse, Druid, Pinot, or other columnar OLAP systems can work well for aggregated metrics, but raw high-frequency per-series writes still need careful batching and cardinality controls.

High-Level Architecture

Drag to pan · Ctrl/⌘ + scroll to zoom

Pull and push collection converge at the ingestion gateway. From there, samples are durably appended to a partitioned log, written by TSDB shards into compressed chunks, compacted into blocks, and queried through a cost-limited PromQL-like frontend.

The architecture separates collection, ingestion durability, storage writing, and query execution. Scrape managers handle pull collection and target health. Push gateways accept StatsD, OpenTelemetry, and remote write style traffic from places where pull is inconvenient. Both paths normalize metric types, labels, timestamps, and tenant identity before using the same ingestion pipeline.

The ingestion gateway is intentionally thin and fast. It validates payloads, checks quotas and cardinality limits, resolves or creates series ids, then appends batches to a durable log. TSDB writer shards own ranges or hash partitions of series ids, append samples to in-memory head blocks and write-ahead logs, then flush compressed immutable chunks to hot storage.

Queries go through a frontend that parses PromQL-like expressions, expands label matchers through the series catalog and inverted index, fans out to shard-local stores for recent raw data, and uses object storage rollups for long time ranges. Rollup workers and compaction run asynchronously so they do not block the write path.

Request Flow

  1. 1

    Targets are discovered and assigned

    Service discovery finds Kubernetes pods, VMs, databases, load balancers, and exporters. The control plane assigns targets to scrape managers with intervals, authentication settings, and static labels such as service, cluster, region, and environment.

  2. 2

    Metrics are collected by pull or push

    In pull mode, a scrape manager periodically calls a target metrics endpoint and parses counter, gauge, histogram, and summary samples. In push mode, applications or agents send batched metrics to the push gateway. Both modes produce normalized sample batches.

  3. 3

    Samples are validated and normalized

    The ingestion gateway checks tenant credentials, payload size, metric naming rules, type consistency, label count, label length, timestamp bounds, and duplicate samples. It sorts labels into canonical order so the same series always maps to the same identity.

  4. 4

    Series identity and cardinality limits are enforced

    The gateway looks up the metric name plus label set in the series catalog. If it is new, it creates a series id only when tenant quotas, per-metric limits, and blocked-label policies allow it. Rejected series are counted and surfaced to users.

  5. 5

    Batches are durably appended

    Accepted samples are appended to a partitioned ingestion log or local write-ahead log before acknowledgement. The partition key is derived from tenant and series id so samples for one series are ordered and routed to the same TSDB writer shard.

  6. 6

    Writer shards build compressed chunks

    TSDB writers consume their partitions, append samples to in-memory head chunks, and periodically flush immutable chunks. Timestamps use delta-of-delta compression and floating-point values use XOR compression, which works well for stable scrape intervals and slowly changing values.

  7. 7

    Queries resolve labels to series

    A query frontend parses the expression, identifies metric selectors and label matchers, and uses the inverted index to find matching series ids. It rejects queries that expand beyond tenant or global cost limits.

  8. 8

    Raw chunks or rollups are scanned

    For recent short windows, the query engine reads raw compressed chunks from hot TSDB shards. For long windows or coarse dashboard steps, it reads precomputed rollup blocks from object storage. The frontend merges partial results, applies aggregations, and returns time series values.

  9. 9

    Compaction, downsampling, and retention run asynchronously

    Background workers compact small chunks into larger immutable blocks, create 5-minute and 1-hour rollups, delete expired raw data, and move cold blocks to cheaper storage. These jobs are rate-limited so they do not disrupt ingestion.

Core Components

Scrape Manager

Performs pull-based collection from targets.

Scrape managers receive target assignments, schedule scrapes with jitter, handle authentication, parse exposition formats, and report scrape health. They should avoid synchronized scraping storms by spreading targets over the interval.

Push Gateway

Accepts pushed metrics from agents and short-lived jobs.

The push gateway handles StatsD-style datagrams, OpenTelemetry metrics, and remote write batches. It batches samples, applies tenant limits, and forwards normalized samples to the ingestion gateway. It should not become the durable source of truth.

Ingest Gateway

Validates samples and appends accepted batches durably.

This stateless tier checks schemas, timestamps, labels, metric type consistency, per-tenant quotas, and cardinality budgets. It resolves series ids, rejects dangerous labels, and writes accepted batches to the ingestion log with backpressure.

Series Catalog and Inverted Index

Maps label sets to series ids and label matchers to postings lists.

The catalog stores metric names, labels, types, tenant ownership, active state, and retention policy. The inverted index stores compressed postings lists for label pairs so a query can find matching series without scanning all metadata.

TSDB Storage Engine

Stores high-volume samples in compressed time chunks.

The engine keeps recent samples in a write-optimized head block, persists a write-ahead log, flushes immutable chunks, and compacts blocks over time. Delta-of-delta timestamp encoding and XOR value encoding reduce storage per sample dramatically.

Query Frontend

Executes PromQL-like expressions with cost controls.

The frontend parses queries, estimates cardinality and time range cost, chooses raw or rollup data, fans out to storage shards, merges partial results, and applies functions such as rate, sum by label, and histogram quantiles.

Rollup and Retention Workers

Downsample and expire data for long-term retention.

Workers aggregate raw samples into 5-minute and 1-hour summaries, compact small blocks, move cold data to object storage, and enforce retention. Rollups must preserve enough statistics for rates, averages, min, max, and histogram queries.

Cardinality Controller

Detects and limits explosive label combinations.

This control loop tracks active series by tenant, metric, label name, and label value. It warns on suspicious growth, blocks disallowed labels, samples or drops excessive new series, and gives users actionable reports on the source of cardinality.

Deep Dive

Metric types and correctness

Counters, gauges, histograms, and summaries have different semantics. A counter is monotonically increasing except for resets, so queries usually use rate over a time window. A gauge is an instantaneous value such as memory usage, queue depth, or temperature. A histogram stores counts in configured buckets plus sum and count, which allows aggregation across instances and later percentile estimation. A summary often calculates client-side quantiles, which can be useful locally but is hard to aggregate correctly across many instances.

The ingestion layer should enforce type consistency for a metric name within a tenant. If one service emits request_latency_seconds as a gauge and another emits it as a histogram, queries become misleading. Metadata should record the metric type and reject or quarantine incompatible samples.

Histograms are usually the best fit for distributed percentiles because bucket counts can be summed by service, region, or cluster. Summaries can be retained for application-specific local quantiles, but interview answers should explain why they are not generally aggregatable.

Pull versus push collection

Pull collection, popularized by Prometheus, lets the monitoring system control scrape intervals, discover targets, detect missing targets, and apply backpressure centrally. It is operationally clean for long-running services and Kubernetes pods because the scraper knows whether a target failed to respond.

Push collection is useful for short-lived jobs, serverless functions, network-isolated environments, and StatsD-style UDP clients. It can be simpler for developers but makes liveness harder: if a client stops pushing, the backend must distinguish silence from success unless heartbeat metrics exist.

A production platform can support both by converging on the same internal sample format. The key is to keep push gateways stateless or lightly stateful, avoid unbounded buffering, and make pull scrape health visible as first-class metadata.

Labels and cardinality explosion

A time series is defined by metric name plus the complete label set. Labels such as service, region, endpoint, method, and status are valuable because they allow slicing and aggregation. Labels such as user_id, request_id, session_id, trace_id, or raw URL can create one series per request and overwhelm the system.

Cardinality grows multiplicatively. If a metric has 100 services, 20 endpoints, 5 status classes, 10 regions, and 4 methods, it can create 400,000 series before considering instances. Adding a label with 1M possible user ids makes the metric impossible to store and query economically.

Protect the system with per-tenant active series quotas, per-metric limits, label name allowlists or denylists, value length limits, high-cardinality detection, and ingestion-time rejection for new series once budgets are exceeded. Good user feedback matters: teams need to know which metric and label caused the explosion.

TSDB storage engine and compression

Metrics are append-heavy and mostly ordered by time. A TSDB writer keeps an in-memory head block for recent samples and a write-ahead log for crash recovery. Periodically it cuts immutable chunks or blocks, writes them to disk, and later compacts them into larger files.

Delta-of-delta timestamp compression stores the change in interval rather than every full timestamp. With a stable 15-second scrape interval, the delta-of-delta is often zero or very small. XOR value compression stores the XOR between neighboring floating-point values, which is compact when values change slowly or repeat. These techniques can reduce samples from 16 raw bytes to a few bytes.

Labels should not be repeated on each sample. Store labels once in the series catalog and index, then store sample chunks by series id. This avoids huge write amplification and keeps the hot path append-oriented.

Query engine and PromQL-like execution

A query engine first resolves selectors. For example, a selector for metric http_requests_total with labels service equals checkout and region equals us-east-1 becomes an intersection of postings lists from the inverted index. The result is a set of series ids to read.

The engine then fetches chunks for the requested time range, decodes samples, aligns them to evaluation steps, and applies functions. Rate calculations need enough points before and after counter resets. Aggregations such as sum by service merge series while preserving chosen grouping labels.

The main risk is fanout. A small-looking query can match millions of series if a label matcher is broad. The frontend should estimate cost before execution, enforce maximum series and sample counts, split work by time and shard, cache common dashboard results, and cancel queries that exceed budget.

Downsampling, rollups, and retention tiers

Raw 15-second samples are valuable during active incidents but expensive for multi-year retention. Older data is usually queried at coarser resolution, such as 5 minutes or 1 hour. Rollup workers can aggregate count, sum, min, max, last value, and histogram buckets into lower-resolution blocks.

Rollups are not just storage compression; they are also query acceleration. A 30-day dashboard with a 5-minute step should not decode every 15-second raw sample if a precomputed 5-minute rollup exists. The query planner should choose the lowest-cost resolution that satisfies the requested step and accuracy.

Downsampling can lose detail. Spikes shorter than the rollup interval may be hidden unless min and max are retained. Percentiles require histograms or sketches, not just average values. A good design clearly states what accuracy is preserved in each tier.

Scaling

Prototype: single-node TSDB

Start with one Prometheus-like server that scrapes targets, stores local TSDB blocks on SSD, and serves queries. This is simple and reliable for a small team, but storage, query, and ingestion compete on the same node.

Team scale: replicated scrapers and remote write

Add multiple scrape managers for availability, push gateways for short-lived jobs, and remote write to a central backend. Use a durable ingestion log so local scrape failures or backend restarts do not immediately lose samples.

Organization scale: sharded distributed TSDB

Partition series by tenant and series id across many writer shards. Store metadata in a shared catalog, maintain an inverted index, replicate hot chunks, and route queries through a frontend that fans out only to relevant shards.

Long retention scale: object storage and rollups

Move compacted immutable blocks and downsampled rollups to object storage. Keep recent raw chunks on SSD for fast incident debugging. Use tier-aware query planning so month-long dashboards read rollups instead of raw samples.

Global scale: federation and tenant isolation

Run regional ingestion clusters close to workloads and federate selected aggregates to a global view. Keep raw data regional when possible, replicate critical service-level aggregates, and enforce per-tenant quotas across regions.

Bottlenecks & Optimizations

Cardinality explosion from unbounded labels

Apply label policies at ingestion, maintain active series budgets, block known dangerous labels, cap label values, and alert users on the top metrics by new series growth. Prefer exemplars or traces for request-level identifiers instead of metric labels.

Hot ingestion shards

Shard by a hash of tenant and series id rather than metric name alone. Add virtual shards, rebalance partitions gradually, and isolate very large tenants. Batch writes and keep samples for one series ordered on a single shard.

Write amplification from indexes and compaction

Store labels once per series, append samples to chunks, batch index updates, and compact immutable blocks in the background. Rate-limit compaction and rollup workers so they do not compete with foreground writes.

Broad queries matching millions of series

Use postings-list indexes, query cost estimation, maximum series limits, result caching for dashboards, and rollup selection for large time ranges. Require users to add selective labels for expensive queries.

Scrape storms at interval boundaries

Add jitter to scrape schedules, spread targets across the interval, and apply per-target timeout budgets. Avoid all scrapers firing exactly every 15 seconds because synchronized scrapes create network and ingest spikes.

High-cardinality histograms

Standardize histogram buckets, limit label combinations on bucket metrics, and aggregate bucket counts before long retention. Native histograms or sketches can reduce bucket explosion, but they still need tenant-level controls.

Failure Handling

TSDB writer shard crashes

Replay the shard's ingestion log partition or write-ahead log on a replacement worker. Keep partition ownership in a coordinator, use checkpointed offsets, and make sample writes idempotent by series id and timestamp.

Ingestion log is unavailable

Gateways should apply backpressure and return retryable errors rather than accepting samples that cannot be made durable. Agents can buffer for a bounded time, but the system should surface ingestion freshness gaps clearly.

Series catalog or index is degraded

Existing series can continue writing from cached mappings for a short period, but new series creation should be restricted. Queries that need fresh label discovery may return partial results with warnings until the index catches up.

Object storage is slow or unavailable

Recent dashboards should continue from hot TSDB storage. Long-range historical queries can degrade or fail gracefully. Compaction and rollup workers should retry with exponential backoff and avoid deleting source blocks before successful writes are verified.

Query frontend overload

Protect ingestion by isolating query resources. Enforce concurrency limits, per-tenant query budgets, caching, cancellation, and priority lanes for alert evaluation. Return clear errors for queries that exceed cost limits.

Bad client emits millions of new series

Cardinality controllers should detect sudden new-series growth, block new series for the tenant or metric, and keep accepting known safe series if possible. Notify owners with examples of offending labels and suggested fixes.

Security

Tenant authentication and authorization

All push, scrape, metadata, and query APIs need authenticated tenant identity. Query authorization should restrict users to allowed workspaces and prevent cross-tenant label or series discovery.

Transport security

Scrapes and pushes should use TLS where possible, with mutual TLS or signed tokens for agents. Scrapers need secure handling of credentials used to reach targets.

Sensitive data in labels

Labels can accidentally contain emails, customer ids, access tokens, paths, or IP addresses. Enforce label policies, value length limits, redaction rules, and retention controls. Do not treat metric labels as a safe place for personal data.

Ingestion denial of service

Attackers or buggy clients can send huge payloads, future timestamps, too many labels, or endless new series. Use payload limits, timestamp windows, quotas, rate limits, and per-tenant isolation.

Query abuse prevention

Expensive queries can consume CPU, memory, and object storage bandwidth. Require authentication, estimate query cost, cap matched series and samples, and audit repeated heavy queries.

Auditability

Record changes to scrape configs, retention policies, label limits, dashboard permissions, and alert rules. During incidents, teams need to know whether missing data came from a system failure, a config change, or an intentional quota.

Tradeoffs

Pros

  • +Append-oriented TSDB storage fits high-volume time-series writes.
  • +Pull and push collection cover both long-running services and short-lived jobs.
  • +Label indexes enable flexible PromQL-like filtering and aggregation.
  • +Downsampling and object storage make long retention economically feasible.
  • +Sharding by series keeps samples ordered while scaling ingestion horizontally.

Cons

  • Label flexibility creates cardinality risk and complex quota management.
  • PromQL-like queries can fan out unpredictably and require strong cost controls.
  • Rollups reduce cost but can hide short spikes or lose percentile accuracy if designed poorly.
  • Distributed TSDB operation adds compaction, index consistency, and shard rebalancing complexity.
  • Supporting both push and pull increases product and operational surface area.

Alternatives

Alternative one is a pure Prometheus-per-team model. It is simple, easy to operate for small teams, and excellent for local reliability, but global querying, long retention, and cross-team governance become harder.

Alternative two is a StatsD-only push pipeline into a stream processor and columnar OLAP store. It is easy for applications to emit counters and timers, but target liveness, scrape health, and rich label discovery are weaker than pull-based monitoring.

Alternative three is to store metrics as logs and aggregate them later. This can reuse logging infrastructure, but it is far more expensive for high-frequency numeric time series and usually cannot match TSDB query latency or compression.

When not to use this design

Do not use a metrics platform as the source of truth for billing, financial transactions, audit logs, or request-level debugging. Metrics are aggregated, sampled, rolled up, and eventually expired. Use logs, traces, event streams, or transactional databases when every individual event must be preserved exactly.

Follow-up Questions

How do you define a unique time series?

A unique series is the combination of tenant, metric name, and the complete normalized label set. Labels must be sorted and canonicalized so equivalent sets produce the same series id. The metric type should also be tracked and kept consistent.

Why is high cardinality dangerous?

Every new label combination creates a new series with metadata, index entries, chunks, memory overhead, and query fanout. One unbounded label such as request_id can create millions of series, increasing cost and slowing queries even if each series has only a few samples.

When would you prefer pull collection over push collection?

Prefer pull for long-running services because the monitoring system controls intervals, discovers targets, and knows when a target disappears. Use push for short-lived jobs, serverless, network-restricted environments, and legacy StatsD-style clients.

Why are histograms usually better than summaries for fleet-wide percentiles?

Histogram bucket counts can be summed across instances and then used to estimate quantiles. Client-side summary quantiles are already aggregated locally and cannot be combined accurately across many instances.

How should the system shard writes?

Shard by tenant and series id hash so samples for one series stay ordered on the same writer while different series spread across many shards. Use virtual shards and gradual rebalancing to handle growth and tenant skew.

How do you answer a 30-day dashboard query efficiently?

Use downsampled rollups when the requested step is coarse enough. The query planner can read 5-minute or hourly rollup blocks from object storage instead of decoding every raw 15-second sample for the full range.

What happens to late or out-of-order samples?

Allow a bounded lateness window and write late samples into the current mutable head block or a small correction block. Very old samples should be rejected or sent to a backfill path because rewriting compacted blocks causes high write amplification.

Company Variations

Amazon

Amazon interviewers often push on operational excellence, multi-AZ durability, cost, noisy-neighbor isolation, and DynamoDB or Kinesis-style partitioning. Be ready to explain ingestion backpressure, shard ownership, and alarms for cardinality explosions.

Microsoft

Microsoft may frame the design around Azure Monitor, enterprise tenants, compliance, role-based access control, and hybrid cloud agents. Discuss secure scraping, workspace isolation, retention policies, and integration with alerting.

Databricks

Databricks is likely to probe large-scale time-series analytics, lakehouse storage, compaction, query planning, and separation of compute from storage. Emphasize immutable blocks, rollups, columnar or object storage tiers, and efficient aggregation.

Snowflake

Snowflake-style discussions may focus on multi-tenant data isolation, elastic query compute, cost governance, and long-retention analytics. Explain how raw ingest, downsampled aggregates, and metadata indexes can be separated for independent scaling.

Interview Tips

Lead with the core data model: metric name plus labels becomes a series, and samples append over time. Then draw the ingestion path, emphasizing durability and cardinality checks before storage. After that, explain the query path with inverted indexes and rollups. Use concrete numbers early because capacity math makes the tradeoffs clear: millions of series and 15-second scrapes quickly become billions of samples per day.

What interviewers expect

  • State assumptions for active series, scrape interval, sample rate, compression, and retention.
  • Describe both pull scraping and push ingestion, then converge them into one pipeline.
  • Explain series identity, labels, cardinality explosion, and quota enforcement.
  • Use a TSDB storage engine with WAL, chunks, delta-of-delta timestamps, and XOR values.
  • Separate ingestion, storage writing, rollups, and query serving.
  • Discuss sharding by series, federation, and rollup tiers for scale.

Common mistakes

  • !Treating labels as free-form without discussing cardinality limits.
  • !Repeating labels on every sample instead of storing series metadata once.
  • !Using summaries for global percentile aggregation without explaining the accuracy problem.
  • !Letting expensive queries compete directly with ingestion resources.
  • !Ignoring downsampling and retention tiers for long-term storage cost.
  • !Designing only push or only pull without acknowledging the tradeoffs.

Red flags

  • ×No concrete sample rate, storage, or retention math.
  • ×No distinction between counters, gauges, histograms, and summaries.
  • ×No inverted index or equivalent way to resolve label matchers.
  • ×No plan for out-of-order samples, writer crashes, or replay.
  • ×No cost controls for high-cardinality queries.

Revision Notes

  • A metrics platform stores time series: tenant plus metric name plus normalized labels maps to a series id, and samples append by timestamp.
  • Metric types matter: counters need rate and reset handling, gauges are instantaneous, histograms aggregate across instances, and summaries are hard to aggregate globally.
  • Pull scraping gives central control and target health; push ingestion is better for short-lived jobs, serverless, network boundaries, and StatsD-style clients.
  • Cardinality is the biggest product and infrastructure risk. Unbounded labels such as user_id, request_id, trace_id, and raw path can create millions of series.
  • With 3M active series and 15-second scrapes, average ingestion is about 200,000 samples per second and 17.3B samples per day.
  • Raw 16-byte timestamp and value pairs would be about 276 GB per day before compression. TSDB compression can reduce chunks to roughly 35 to 70 GB per day before replication and indexes.
  • The write path should validate, resolve series ids, append to a durable log, and route by series id to TSDB writer shards.
  • The TSDB engine uses a write-ahead log, in-memory head chunks, immutable blocks, compaction, delta-of-delta timestamps, and XOR value compression.
  • Query execution resolves label selectors through an inverted index, reads raw chunks or rollups, applies functions and aggregations, and enforces cost limits.
  • Long retention needs downsampling: keep raw data for recent debugging, 5-minute rollups for medium retention, and hourly rollups for long-term trends.
  • Scale with shard ownership, virtual shards, regional ingestion, object storage blocks, federation for global views, and tenant-level quotas.

Flashcards

Quiz

0/7 answered

  1. 1.What creates a new time series in a metrics platform?

  2. 2.Which label is most likely to cause cardinality explosion?

  3. 3.Why are histograms generally easier to aggregate across instances than summaries?

  4. 4.With 3M active series and a 15-second scrape interval, what is the average ingest rate?

  5. 5.Which storage technique is most specific to efficient TSDB sample storage?

  6. 6.What should the query engine do before executing a broad label selector?

  7. 7.Why are rollups useful for long-range dashboards?

Cheat Sheet

Goal: collect, store, and query high-volume time-series metrics from services and infrastructure.

Data model: tenant plus metric name plus normalized labels equals one series. Samples are timestamp and value pairs appended to that series.

Metric types: counters for monotonically increasing totals, gauges for current values, histograms for aggregatable distributions, summaries for client-side quantiles with limited aggregation.

Collection: pull scraping gives central control and target health. Push ingestion supports short-lived jobs, serverless, agents, and StatsD. Normalize both into one internal sample pipeline.

Capacity anchor: 3M active series at 15-second intervals is about 200,000 samples per second and 17.3B samples per day.

Storage: use WAL, in-memory head chunks, immutable compressed blocks, compaction, and object storage. Use delta-of-delta timestamps and XOR values. Store labels once per series, not per sample.

Indexes: use an inverted index from label name and value to compressed postings lists of series ids. Query selectors intersect postings lists before reading chunks.

Cardinality: enforce quotas on active series, labels per metric, label value length, and new series rate. Block request_id, user_id, trace_id, and raw path labels unless explicitly allowed.

Queries: parse PromQL-like expressions, estimate cost, select raw or rollup data, fan out to shards, merge results, and enforce timeouts.

Retention: keep raw recent data for debugging, 5-minute rollups for months, hourly rollups for years, and delete or compact expired blocks asynchronously.

Scaling: shard by tenant and series id, replicate recent data, isolate large tenants, run regional ingestion clusters, and federate selected aggregates for global views.

References