Metrics and Monitoring System Design Template (Reference Design)
Metrics and alerting design: OTLP and remote write ingestion, Gorilla-style compression, cardinality limits, rollups, alert evaluation, meta-monitoring
How to use: Use it when designing or reviewing a metrics and alerting platform, or preparing the monitoring system design interview. Fill the active-series count and the retention table first, because series count sizes memory and rollups dominate storage. The main pitfall is unlimited label cardinality, which one team can use to take the platform down for everyone.
Preview
System Design: [Metrics and monitoring platform name]
Reference design for a multi-tenant time-series metrics and alerting platform: ingestion over OTLP and Prometheus remote write, sharded in-memory ingesters with a write-ahead log, compressed blocks in object storage, downsampling and retention tiers, cardinality control, rule evaluation and alert routing. It follows the OpenTelemetry metrics data model (stable sections as of OpenTelemetry Specification v1.62.0, October 2026) and the compression ideas published in the Gorilla paper (PVLDB 8(12), 2015). It follows the structure of the System Design Document template, adds a 45-minute interview path, access-pattern and failure-mode tables and a cited "How real systems do it" box, and is pre-filled with a worked design to adapt. Replace every
[bracketed]value and every row marked Example; values marked Example are assumptions, not benchmarks.
Document control
| Field | Value |
|---|---|
| Document ID | [SDD-MON-001] |
| System / feature | [Metrics storage, query and alerting platform] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Owner | [@owner] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Classification | [Internal] |
| Difficulty | Advanced |
| Data model version implemented | [OpenTelemetry Specification v1.62.0, metrics data model] |
| Related | [Observability specification], [Alerting policy], [ADR: series limits] |
Approvals
| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Staff / principal engineer | [@name] | [Decision] | YYYY-MM-DD |
| SRE / operations | [@name] | [Decision] | YYYY-MM-DD |
| Security | [@name] | [Decision] | YYYY-MM-DD |
| FinOps (storage cost) | [@name] | [Decision] | YYYY-MM-DD |
Revision history
| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | Example: Added per-tenant series limits after a label explosion incident |
| 0.3 | YYYY-MM-DD | [@name] | Example: Moved 5-minute rollups to object storage, raw retention cut to 15 days |
| 1.0 | YYYY-MM-DD | [@name] | Example: Approved |
Contents
1. Overview
1.1 Problem statement
[Example: Each product team runs its own metrics server. Retention differs from 3 to 90 days, there is no global view across clusters, and two outages last quarter went unnoticed for 20 minutes because the metrics server of the affected cluster failed together with the cluster. One team added a request ID as a label, created 14 million series in an hour and took down the shared server for everyone.]
1.2 Proposed solution
[Example: A central, multi-tenant platform. Agents and SDKs push metrics over OTLP or Prometheus remote write to a stateless distributor, which validates them, enforces per-tenant limits and hashes each series to three ingesters. Ingesters keep the last two hours in compressed memory blocks with a write-ahead log, then flush immutable blocks to object storage. A compactor merges blocks and writes 5-minute and 1-hour rollups. A query frontend splits, caches and routes queries to ingesters (recent data) and store gateways (old data). A rule evaluator runs recording and alerting rules and sends alerts to an alert router that deduplicates, groups and notifies. A separate, small meta-monitoring stack watches the platform itself.]
1.3 Decisions requested from reviewers
| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | Example: Push or pull ingestion? | Push (OTLP, remote write) into the platform; pull scraping stays inside each cluster agent | 7.1, 10 |
| Q-02 | Example: What happens over the series limit? | Reject new series for that tenant with a clear error; existing series keep flowing | 7.3 |
| Q-03 | Example: Raw retention? | 15 days raw, 13 months at 5 minutes, 5 years at 1 hour | 7.4 |
| Q-04 | Example: Delta or cumulative temporality at storage? | Store cumulative; convert delta streams at the distributor | 7.6 |
| [Q-nn] | [Question] | [Option] | [Section] |
1.4 45-minute interview path
Use this order when the document serves an interview or a short design review. The written sections below are the long form of each step.
| Minutes | Cover | Output on the whiteboard |
|---|---|---|
| 0 to 5 | Scope: which signals (metrics only, not logs or traces), push or pull, who queries, alerting in or out; below-the-line items (2.3) | Three to five requirements |
| 5 to 10 | Estimates: active series, samples per second, bytes per sample, memory for the recent window (section 3) | Four numbers and "series count drives memory" |
| 10 to 15 | Interface: write call, query call, alert rule (section 4) | Two calls and one rule |
| 15 to 25 | High-level design: distributor, ingesters, object storage, querier, rule evaluator (section 6) | One diagram |
| 25 to 40 | One deep dive: compression and the in-memory window (default, 7.2), or cardinality limits (7.3), or downsampling (7.4), or alert evaluation (7.5) | Options, choice, reason |
| 40 to 45 | Failures (ingester crash, cardinality explosion, monitoring down during an outage), the 10x row in section 10 | Two failure rows |
Start the default deep dive with the naive answer: store each sample as a row of (series ID, timestamp, value) in a key-value store, 16 bytes per point before indexes. Show that at 6.7 million samples per second this is about 9.2 TB a day, then introduce delta-of-delta timestamps and XOR values and the two-hour in-memory block.
2. Requirements
2.1 Core functional requirements
| ID | Requirement | Priority |
|---|---|---|
| FR-01 | Example: Accept metrics over OTLP (HTTP and gRPC) and Prometheus remote write, per tenant | Must |
| FR-02 | Example: Support counters (sums), gauges, histograms (explicit and exponential buckets) | Must |
| FR-03 | Example: Query with a PromQL-compatible language over any time range within retention | Must |
| FR-04 | Example: Evaluate recording and alerting rules every 30 s and route alerts to on-call tools | Must |
| FR-05 | Example: Enforce per-tenant limits on active series, samples per second and labels per series | Must |
| FR-06 | Example: Downsample to 5-minute and 1-hour resolution and apply retention per tier | Must |
| FR-07 | Example: Show per-tenant usage (series, samples, top metrics by series count) | Should |
| FR-08 | Example: Silence and inhibit alerts with an audit trail | Should |
| [FR-nn] | [Requirement] | [Must / Should / Could] |
2.2 Non-functional requirements
| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Ingestion availability | Example: 99.9% of write requests accepted per month | Distributor success ratio |
| NFR-02 | Freshness | Example: A sample is queryable within 30 s of receipt at p99 | Synthetic canary series |
| NFR-03 | Query latency | Example: p95 at or below 2 s for a 24-hour dashboard panel over 1 000 series | Query frontend timing |
| NFR-04 | Alert latency | Example: Alert notification within 90 s of the condition becoming true, plus the rule for duration | Canary alert end to end |
| NFR-05 | Durability | Example: No loss of acknowledged samples on a single ingester crash | Chaos test |
| NFR-06 | Isolation | Example: One tenant over its limits cannot raise write latency for others | Noisy-tenant load test |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |
2.3 Below the line (out of scope)
Name these explicitly so reviewers and interviewers do not pull the design toward them.
- [Example: logs and traces (separate pipelines; exemplars link to trace IDs only)]
- [Example: anomaly detection and forecasting on metrics]
- [Example: per-user product analytics (event data, not metrics)]
- [Example: billing-grade usage metering (needs exactly-once accounting)]
- [Item deferred, with the release that will take it]
3. Capacity estimation
3.1 Assumptions
| Input | Symbol | Value | Source |
|---|---|---|---|
| Active series (all tenants) | Example: 100 million | Sum of current servers' head series x growth | |
| Collection interval | Example: 15 s | Agent default | |
| Uncompressed sample (8-byte timestamp, 8-byte float) | 16 bytes | Data type sizes | |
| Compressed sample in memory | Example: 1.37 bytes | Gorilla paper average, check on your data | |
| Compressed sample on disk including index | Example: 2 bytes | Measured on a sample block | |
| Head memory per active series (labels, index, chunks) | Example: 4 KiB | Measured on target ingester build | |
| Usable memory per ingester | Example: 48 GiB | 64 GiB instance minus runtime overhead | |
| Replication factor for ingesters | 3 | Survive one ingester loss with quorum writes | |
| Alerting and recording rules | Example: 50 000 | Current rule files x growth |
3.2 Ingestion rate
With replication each sample is written to three ingesters, so the ingester tier absorbs sample appends per second. At an assumed 10 bytes per sample on the wire (protobuf plus snappy) the distributors receive about 66.7 MB/s. Consequence: the write path is CPU-bound on decompression and hashing, not network-bound, so distributors scale on CPU.
3.3 Storage and retention
| Data | Formula | Per day | Retention | Total |
|---|---|---|---|---|
| Raw, uncompressed (for comparison) | B | 9.2 TB | none | not stored |
| Raw, compressed in memory | B | 789 GB | 2 h in memory | see 3.4 |
| Raw blocks on object storage | B | 1.15 TB | 15 days | 17.3 TB |
| 5-minute rollups (5 aggregates, 10 B per rollup) | B | 288 GB | 13 months (395 days) | 113.8 TB |
| 1-hour rollups (5 aggregates, 10 B per rollup) | B | 24 GB | 5 years (1 826 days) | 43.8 TB |
| Total in object storage | about 175 TB |
Compression cuts the raw stream by times, which is what makes an in-memory recent window affordable. Rollups, not raw data, are 90% of the stored bytes, so the retention of the 5-minute tier is the main cost lever (section 9.4). Consequence: raw data goes to object storage and the 13-month tier is reviewed with dashboard owners before anyone asks for longer raw retention.
3.4 Memory for the recent window
The factor 1.25 is headroom for uneven series distribution and for series churn (pods restarting create new series while old ones are still in the head). Each ingester appends about samples per second. Consequence: memory per series, not samples per second, decides the ingester count, so the series limit in 7.3 is a capacity control, not only a safety net.
3.5 Queries and rules
- Rule evaluations: per second, mostly over the last few minutes, so they are served from ingesters.
- A 7-day panel at 15 s resolution reads 40 320 points per series; at 5-minute resolution it reads 2 016, 20 times fewer. A 1 000-series panel at raw resolution reads 40.3 million points. Consequence: the query frontend routes ranges longer than 2 days to the 5-minute tier automatically.
3.6 Summary
| Quantity | Estimate | Design consequence |
|---|---|---|
| Samples | about 6.7 million/s (20 million appends/s with RF 3) | Stateless distributors scaled on CPU; hash by series |
| Recent window memory | about 1.1 TiB with RF 3 | 30 ingesters with 48 GiB usable each |
| Object storage | about 175 TB, 90% rollups | Retention of the 5-minute tier is the cost lever |
| Rule evaluations | about 1 700/s | Rules read from ingesters; sharded rule evaluators |
| Long-range queries | 20x fewer points from 5-minute rollups | Automatic resolution selection in the frontend |
4. API design
4.1 Interfaces
| Interface | Operation | Purpose |
|---|---|---|
| Ingestion | POST /v1/metrics (OTLP/HTTP, protobuf) and OTLP/gRPC MetricsService/Export | Push metrics from SDKs and collectors |
| Ingestion | POST /api/v1/push (Prometheus remote write) | Push from Prometheus-compatible agents |
| Query | GET /api/v1/query?query=&time= and /api/v1/query_range?query=&start=&end=&step= | Instant and range queries |
| Metadata | GET /api/v1/series, /api/v1/labels, /api/v1/label/{name}/values | Discovery for dashboards |
| Rules | PUT /api/v1/rules/{namespace} | Create or replace a rule group (versioned) |
| Alerts | GET /api/v1/alerts, POST /api/v1/silences | Active alerts and silences |
| Usage | GET /api/v1/tenants/{id}/usage | Series count, samples/s, top metrics by series |
Every request carries a tenant identity derived from the authenticated credential, never from a free-text header set by the client.
4.2 Write responses and limits
A write that would create series beyond the tenant's limit is partly
accepted: existing series are stored, new series are dropped, and the
response says so. Because the stored part must not be sent again, this is
not a retryable error. On OTLP the response is 200 OK with the
partial_success field filled in (rejected_data_points and
error_message), which the OTLP specification says clients must not retry;
on Prometheus remote write it is a 400, which senders do not retry. Keep
429 with Retry-After for the samples-per-second rate limit (7.3), where
the whole request was refused and a later retry is correct. Example OTLP
response body, shown in the OTLP JSON encoding:
{"partialSuccess": {"rejectedDataPoints": "1240", "errorMessage": "Tenant team-checkout has 5000000 active series (limit 5000000). 1240 data points for new series were dropped; 18760 data points for existing series were stored. Retrying will not help: reduce label values or ask for a higher limit."}}
4.3 Alert rule format
Rules are versioned documents; a new rule group starts in shadow mode,
where it evaluates and records what it would have fired without notifying.
{"namespace": "checkout", "group": "slo-burn", "interval_s": 30, "mode": "enforce", "rules": [{"alert": "CheckoutErrorBudgetFastBurn", "expr": "sum(rate(http_server_request_duration_seconds_count{service=\"checkout\",status_class=\"5xx\"}[5m])) / sum(rate(http_server_request_duration_seconds_count{service=\"checkout\"}[5m])) > 14.4 * 0.001", "for_s": 120, "labels": {"severity": "page"}, "annotations": {"runbook": "https://runbooks.example.com/checkout/error-budget"}}]}
The threshold 14.4 * 0.001 is a fast-burn alert for a 99.9% SLO: an error
ratio 14.4 times the budget spends 2% of a 30-day budget in one hour.
5. Data model
5.1 Entities
A series is identified by its metric name plus the full set of label (attribute) pairs, including resource attributes such as service and cluster. Every distinct combination is a new series, which is why label values must come from small, bounded sets.
5.2 Storage choices
| Data | Store | Key | Main queries | Why |
|---|---|---|---|---|
| Recent samples (2 h) | Example: Ingester memory plus local write-ahead log | Series ref, time | Rules, live dashboards | Compressed chunks fit in memory; WAL replays after a crash |
| Older samples and rollups | Object storage, immutable blocks | Tenant, block time range, resolution | Long-range dashboards | Cheap, durable, no servers to scale for capacity |
| Block index (postings) | Inside each block plus a bucket index cached by store gateways | Label name and value to series refs | Find series matching a selector | Inverted index is what makes label queries fast |
| Rules, silences, limits | Relational database | Namespace, group, version | Load on change | Small, audited |
| Query results | Cache keyed by query, step-aligned range | Query hash, time split | Repeated dashboard loads | Dashboards refresh the same ranges every 30 s |
5.3 Access patterns
| Access pattern | Frequency | Key or index that serves it | Partition key rationale |
|---|---|---|---|
| Append a sample | 6.7 million/s (20 million appends with RF 3) | Series ref in the ingester head | Hash of tenant plus full label set spreads series evenly across ingesters |
| Evaluate a rule over the last 5 minutes | about 1 700/s | Ingester head postings index | Recent data lives only in ingesters; queriers fan out to the replicas that own matching series |
| Dashboard panel over 24 hours | Thousands per minute | Ingesters plus the newest blocks, results cached per step-aligned split | Splitting by day makes cached splits reusable |
| Dashboard over 30 days | Hundreds per minute | 5-minute rollup blocks through store gateways | Rollups are partitioned by tenant and time, so a range touches few blocks |
| Find label values for a dropdown | Frequent | Postings index, label name to values | Served from index headers cached in store gateways |
| Usage report per tenant | Every minute | Ingester head series counters per tenant | Counted where series are created; no scan |
6. High-level design
| Component | Responsibility | Scales by |
|---|---|---|
| Distributor | Authenticate tenant, validate, enforce limits, convert temporality, hash series to ingesters | CPU, stateless |
| Ingester | Hold the 2-hour head in memory, append to WAL, cut and upload blocks | Active series (memory) |
| Compactor and downsampler | Merge blocks, deduplicate replicas, write 5-minute and 1-hour rollups, apply retention | Block volume per tenant |
| Store gateway | Serve queries over blocks in object storage, cache index headers | Blocks queried |
| Query frontend | Split long queries by day, cache results, pick resolution, queue per tenant | Query rate |
| Querier | Execute the query language over ingesters and store gateways | Query complexity |
| Rule evaluator | Run recording and alerting rule groups on schedule | Number of rule groups |
| Alert router | Deduplicate, group, inhibit, silence and notify | Alerts per minute |
| Meta-monitoring stack | Watch the platform from outside its failure domain | Small, fixed |
7. Detailed design
The deep dives in this section, each with options, choice and reason:
| Deep dive | Options considered | Choice | Reason | Section |
|---|---|---|---|---|
| Write path | Pull scraping by a central server, push to a key-value store, push to sharded in-memory ingesters | Push to ingesters with WAL and RF 3 | Recent data is read most; memory serves rules fast | 7.1 |
| Encoding | Row per sample, general-purpose compression, delta-of-delta and XOR chunks | Delta-of-delta timestamps, XOR values | About 1.37 bytes per sample with fast sequential decode | 7.2 |
| Cardinality | No limits, global limit, per-tenant and per-metric limits with drop of new series | Per-tenant and per-metric limits | Protects memory without losing existing series | 7.3 |
| Retention | Keep raw forever, fixed TTL, tiered rollups | Tiered rollups with correct aggregates | Long-range queries 20x cheaper; storage dominated by rollups | 7.4 |
| Alerting | Evaluate in each agent, central rule evaluator, streaming evaluation | Central evaluator with shadow mode and meta-monitoring | One place for rules, silences and audit | 7.5 |
7.1 Write path
- The distributor authenticates the tenant and checks request size, label count (Example: at most 30 labels per series), label name syntax and value length (Example: 1 024 bytes).
- It converts delta-temporality sums to cumulative per series (7.6) and rejects samples older than the head window or too far in the future.
- It hashes tenant plus sorted label set onto a consistent-hash ring and sends the sample to the three ingesters that own the token. The write succeeds when two of three acknowledge.
- Each ingester appends to its write-ahead log, then to the in-memory head. Every two hours it cuts an immutable block, uploads it and truncates the WAL once upload is confirmed.
Because three replicas hold the same series, the compactor deduplicates overlapping blocks from the replicas, keeping one sample per timestamp.
7.2 Compression and the in-memory window
The encoding follows the scheme published for Gorilla (paraphrased):
- Timestamps are stored as the difference of consecutive differences ("delta of delta"). For a series collected every 15 s the deltas are 15, 15, 15, so the delta of delta is 0 and costs one bit. Small irregularities fall into short bit ranges with a short prefix; only large jumps use a long form.
- Values are XORed with the previous value. Identical values cost one bit; similar floats share sign, exponent and leading mantissa bits, so only the changed middle bits are stored with their position.
Example: timestamps 1 000, 1 015, 1 030, 1 045, 1 061 have deltas 15, 15, 15, 16 and deltas of delta 0, 0, 1. After the first two timestamps the next three cost 1 + 1 + 9 bits (a 2-bit prefix and a 7-bit value for the 1), or 11 bits instead of 192 bits uncompressed.
Chunks are aligned to two-hour windows. Decoding is sequential, which suits range scans and rules over the last minutes, and costs nothing for random access because nobody reads one sample in the middle of a chunk. The paper reports an average of 1.37 bytes per sample against 16 uncompressed; measure your own ratio, since counters with many distinct values compress worse than mostly constant gauges.
7.3 Cardinality control
Cardinality is the number of distinct series. It explodes multiplicatively:
Example: http_server_request_duration_seconds with 5 methods, 10 status
codes, 200 routes and 2 000 pods yields
series, 20% of the whole platform budget in one metric. Adding a user ID label
makes it unbounded. Prometheus naming guidance warns against labels with
many distinct values such as user IDs and email addresses; put those in logs
or traces and link them with exemplars.
| Limit | Example default | Behaviour when exceeded |
|---|---|---|
| Active series per tenant | 5 million | New series dropped, existing series stored; OTLP partial success or remote-write 400, not retried (4.2) |
| Active series per metric name per tenant | 500 000 | Same, plus usage report names the top labels by value count |
| Labels per series | 30 | Whole series rejected |
| Samples per second per tenant | 500 000, burst 1 million | Token bucket at the distributor, 429 with Retry-After |
Limits are counted in the ingesters, where series are created, and the
distributor reads them from a shared view refreshed every few seconds, so a
tenant can overshoot by the series created in one refresh interval.
Distributor-side relabelling rules let a tenant drop or aggregate a label
(for example remove pod and keep deployment) before series are created.
7.4 Downsampling and retention
| Tier | Resolution | Retention | Aggregates kept | Used for |
|---|---|---|---|---|
| Head | Native (15 s) | 2 h in memory | Raw samples | Rules, live dashboards |
| Raw blocks | Native | 15 days | Raw samples | Incident investigation |
| 5-minute rollups | 300 s | 13 months | min, max, sum, count, last (counters: increase over the interval) | Capacity trends, monthly reviews |
| 1-hour rollups | 3 600 s | 5 years | Same as 5-minute | Year-over-year planning |
Rollups must keep aggregates that can be combined again. An average of averages is wrong when sample counts differ, so the store keeps sum and count and computes the average at query time. Counters are rolled up as the increase within the interval after handling resets, never as the raw counter value. Histograms are rolled up by adding bucket counts, which is only valid when bucket boundaries match; exponential histograms with different scales are first converted to the coarser scale. Percentiles cannot be rolled up from stored percentiles; keep the histogram.
7.5 Alert evaluation and watching the watcher
- Rule groups are sharded across evaluators by hash of namespace and group.
Each group runs every
interval_s; a missed evaluation is recorded as a metric and alerts on itself. - An alert becomes pending when the expression returns a result and firing
after it stays true for
for_s. Alerts on absence (absent()or a canary series) catch a silent exporter. - The alert router deduplicates by label fingerprint, groups related alerts into one notification, applies inhibition (a cluster-down alert suppresses its per-pod alerts) and silences with an owner and expiry.
- Meta-monitoring runs in a different region or provider with its own small storage. It probes the write path with a canary series, the query path and the alert path with a canary alert that must arrive at a test receiver every 5 minutes (a "dead man's switch"). If the canary stops arriving, the meta stack pages directly.
7.6 Temporality and the OpenTelemetry data model
The OpenTelemetry metrics data model defines Sum, Gauge, Histogram and ExponentialHistogram point kinds, plus Summary kept for compatibility and not recommended for new applications. Sums carry an aggregation temporality: cumulative points repeat the same start time, delta points advance it. This platform stores cumulative sums because rate functions and rollups expect them. The distributor keeps the last cumulative total per delta series and adds each delta to it; if the start time does not follow the previous end time it records a reset. That state lives in the distributor that receives the stream, so delta sources must stick to one distributor (by consistent hashing on the series at the load balancer) or send cumulative data, which is the safer default.
7.7 How real systems do it
How real systems do it (paraphrased, not copied). The Gorilla paper (Pelkonen and co-authors, PVLDB 8(12), 2015) describes the in-memory time-series store that sat in front of an HBase-backed monitoring system at a large social network. It argues that monitoring users care most about recent data and aggregates, so the store keeps the most recent 26 hours in memory, accepts losing a small amount of data on the write path in exchange for availability, and compresses timestamps with delta of delta and values with XOR. The authors report about 96% of timestamps compressed to one bit, roughly 51% of values to one bit, an average of 1.37 bytes per point against 16, and query latency 73 times lower than the previous system. Every write is streamed to two independent instances in separate regions, and queries move to the other region when one is unhealthy. Google's Monarch paper (Adams and co-authors, PVLDB 13(12), 2020) describes a planet-scale in-memory time-series system that keeps data in regional zones close to where it is produced, with global query and configuration planes on top, so a regional problem stays regional. This design keeps both lessons: the recent window in memory (7.2) and a monitoring path for the monitor that does not share its failure domain (7.5). Read the originals in 12.3.
8. Scalability and performance
| Concern | Approach | Limit before redesign |
|---|---|---|
| Sample rate | Stateless distributors, hash ring | Example: about 667 000 appends/s per ingester at 30 ingesters |
| Active series | Memory per series, per-tenant limits | 48 GiB usable per ingester; add ingesters before 80% |
| Series churn | Short-lived pods create series that stay in the head until the next cut | Churn above 30% of head per 2 h doubles memory need; aggregate the pod label |
| Long-range queries | Day splits, result cache, rollup selection | Queries over 13 months at 5-minute resolution across 100 000 series |
| Index lookups | Postings in block index, index headers cached in store gateways | Regex selectors over high-cardinality labels |
| Rule load | Sharded evaluators, recording rules for expensive expressions | Rule groups slower than their interval |
Query latency budget for a 24-hour panel over 1 000 series (target p95 2 s):
| Step | Budget |
|---|---|
| Frontend split by day and cache lookup | 0.05 s |
| Querier fan-out to ingesters (last 2 h) | 0.3 s |
| Store gateway reads for the remaining 22 h | 0.9 s |
| Evaluation and merge | 0.35 s |
| Headroom | 0.4 s |
9. Reliability, security and operations
9.1 Failure modes
| Component | Failure | Detection | Blast radius | Mitigation | RTO |
|---|---|---|---|---|---|
| Ingester | Crash or node loss | Ring health, missing heartbeats | None for writes (2 of 3 quorum); queries read remaining replicas | WAL replay on restart; ring hands tokens to a new ingester | Example: 5 min to rejoin |
| Ingesters | Two replicas of the same token range down | Write quorum failures by token range | Writes fail for series whose three replicas include both failed ingesters, a small share of all series | Spread replicas across zones; zone-aware ring | Example: 10 min |
| Tenant | Cardinality explosion | Series creation rate per tenant | That tenant's new series | Per-tenant and per-metric limits (7.3) | Immediate (limit) |
| Object storage | Regional degradation | Upload error rate, block upload lag | Ingesters keep blocks locally longer; long-range queries fail | Local disk sized for 6 h of blocks; retry with backoff | Example: provider dependent |
| Compactor | Stuck or failing | Oldest uncompacted block age | Slower queries, higher storage cost | Alert at 24 h lag; compaction is idempotent and restartable | Example: 1 h |
| Rule evaluator | Missed evaluations | Missed iteration counter | Alerts in the affected rule groups delayed | Shard rebalancing; alert on missed iterations | Example: 2 min |
| Whole platform | Down during a production incident | Meta-monitoring dead man's switch | No dashboards or alerts for all tenants | Independent meta stack pages directly; cluster agents buffer locally | Example: 30 min |
| [Component] | [Failure] | [Signal] | [Scope] | [Mitigation] | [Time] |
9.2 Security and privacy
| Threat | Control |
|---|---|
| Tenant reads another tenant's data | Tenant ID from the credential; every query and block path is scoped by tenant |
| Personal data in label values (emails, user IDs, IP addresses) | Distributor deny-list rules for known patterns; guidance to use logs or traces instead; deletion API per series |
| Write flooding as denial of service | Per-tenant sample rate limits and request size limits |
| Tampering with alert rules or silences | Role-based access, versioned rule groups, audit log of silences with owner and expiry |
| [Threat] | [Control] |
9.3 Observability of the platform
| Signal | Metric or log | Alert threshold |
|---|---|---|
| Write success ratio | Accepted / received samples per tenant | Below 99.9% for 10 min |
| Freshness | Age of newest canary sample | Above 60 s |
| Head memory | Bytes in head per ingester | Above 80% of usable |
| Series creation rate | New series per tenant per minute | 10x the 7-day baseline |
| Block upload lag | Oldest block not yet uploaded | Above 4 h |
| Alert path | Canary alert received at test receiver | Missing for 10 min |
9.4 Cost estimate
| Item | Unit cost | Quantity | Monthly cost |
|---|---|---|---|
| Ingesters | [$/node-month] | Example: 30 nodes with 64 GiB memory | [$] |
| Distributors, queriers, store gateways, rule evaluators | [$/node-month] | [Count] | [$] |
| Object storage | [$/TB-month] | Example: about 175 TB | [$] |
| Object storage requests | [$/million requests] | [Uploads plus query reads] | [$] |
| Meta-monitoring stack | [$/month] | 1 small deployment in another region | [$] |
| Total | [$] |
10. Trade-offs and alternatives
| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Ingestion model | Central pull scraping; push to platform | Push to platform, pull only inside clusters | Works across networks and for short-lived jobs | The platform cannot see a target that silently stops sending (needs absence alerts) |
| Recent data | Disk-based TSDB; in-memory head with WAL | In-memory head | Rules and live dashboards read recent data most | Memory cost scales with active series |
| Storage of old data | Local disks on stateful nodes; object storage | Object storage | Capacity without servers, high durability | Higher first-byte latency, needs caching |
| Cardinality | Unlimited; global limit; per-tenant and per-metric limits | Per-tenant and per-metric | One team cannot harm the others | Teams must change instrumentation when they hit limits |
| Temporality | Store delta; store cumulative | Cumulative with conversion at the distributor | Rate and rollups expect cumulative | Stateful conversion for delta sources |
| Query language | New language; PromQL-compatible | PromQL-compatible | Existing dashboards and skills | Inherits its edge cases around staleness |
| What changes at 10x / 0.1x | 10x (1 billion active series, about 66.7 million samples/s): about 298 ingesters by the 3.4 formula, so split into independent cells of about 30 ingesters with tenants assigned to cells, and storage grows to about 1.75 PB. 0.1x (10 million series, about 667 000 samples/s): one replicated pair of single-node time-series servers with remote storage is simpler and cheaper | Review at each doubling of active series | Memory and cell count, not network, grow first | [Note] |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |
11. Delivery plan and risks
11.1 Milestones
| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Write path, ingesters, object storage, query for two pilot tenants | Freshness and query targets met for 14 days | YYYY-MM-DD |
| M2 | Limits, usage API, rollups | Cardinality test tenant contained without effect on others | YYYY-MM-DD |
| M3 | Rule evaluator, alert router, meta-monitoring | Canary alert end-to-end within 90 s | YYYY-MM-DD |
| M4 | All tenants migrated, old servers retired | Dashboards and alerts parity signed off by each team | YYYY-MM-DD |
11.2 Rollout and rollback
[Example: Teams dual-write from their agents to the old server and the platform for 30 days. Rule groups run in shadow mode on the platform and their would-fire results are compared with the old alerts. Cut-over is per team; rollback points dashboards and alert routing back to the old server, which kept receiving data during dual-write.]
11.3 Risks
| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | Example: Series churn from autoscaling doubles head memory | Medium | High | Aggregate pod labels at the distributor; churn alert | [@name] |
| R-02 | Example: Teams rely on 90-day raw retention for audits | Medium | Medium | Interview dashboard owners; audit data belongs in logs | [@name] |
| R-03 | Example: Remote write 2.0 is still an experimental specification | Medium | Low | Accept both 1.0 and 2.0 payloads; pin the revision in docs | [@name] |
| R-04 | Example: Shared platform becomes a single point of failure for all alerting | Low | High | Meta-monitoring in another failure domain; critical alerts also evaluated locally | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |
12. Appendices
12.1 Open questions
| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | Example: Do we accept native or exponential histograms from all agents, or convert at the distributor? | [@name] | YYYY-MM-DD |
| OQ-02 | Example: Which tenants need data residency, and therefore a regional cell? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |
12.2 Glossary
| Term | Meaning |
|---|---|
| Series | One metric name with one exact set of label values |
| Cardinality | Number of distinct series, overall or per metric |
| Head | The in-memory window of recent samples in an ingester |
| Block | An immutable file set covering a time range: chunks plus an index |
| Rollup | A lower-resolution copy that keeps combinable aggregates |
| Temporality | Whether a sum reports totals since start (cumulative) or changes since the last report (delta) |
| Dead man's switch | An alert that always fires; its absence means the alert path is broken |
| [Term] | [Meaning] |
12.3 References
| Reference | Use |
|---|---|
| OpenTelemetry Protocol (OTLP) specification | Partial success and retry rules (4.2) |
| OpenTelemetry metrics data model, OpenTelemetry Specification (latest release v1.62.0, October 2026; page status mixed, core sections stable) | Point kinds, temporality, Summary as legacy (7.6) |
| Prometheus metric and label naming | Cardinality guidance (7.3) |
| Prometheus Remote-Write 2.0 specification, marked experimental, 2.0-rc.6 as of October 2026 | Ingestion interface (4.1) |
| Pelkonen, Franklin, Teller, Cavallaro, Huang, Meza, Veeraraghavan, "Gorilla: A Fast, Scalable, In-Memory Time Series Database", PVLDB 8(12): 1816 to 1827, 2015, doi:10.14778/2824032.2824078 | 7.2, 7.7 |
| Adams and co-authors, "Monarch: Google's Planet-Scale In-Memory Time Series Database", PVLDB 13(12): 3181 to 3194, 2020 | 7.7 |
| RFC 9457 Problem Details for HTTP APIs | Error bodies for refused requests, such as the rate-limit 429 (7.3) |
Observability Specification (ops/observability-spec) | What each service must emit |
Alerting Policy (ops/alerting-policy) | Paging rules, severities |
Error Budget Policy (ops/error-budget-policy) | Burn-rate alerts in 4.3 |
Stream Processing Engine System Design (system-design/design-stream-processing-engine) | Streaming alternative for rule evaluation |
Distributed Tracing and Logging System Design (system-design/design-distributed-tracing-logging) | Traces and logs linked by exemplars (2.3) |
Back-of-Envelope Estimation and Capacity Model (system-design/back-of-envelope-estimation) | Method for section 3 |
Standard and licence note
Based on the OpenTelemetry metrics data model (OpenTelemetry Specification, latest release v1.62.0 as of October 2026), the public Prometheus documentation and the published Gorilla (PVLDB 2015) and Monarch (PVLDB 2020) papers cited in 12.3. The papers are paraphrased in 7.2 and 7.7 only; no text, figures or code were copied, and all capacity numbers here are illustrative examples computed from the stated assumptions. Check the current specification versions before implementing. Original text written for this collection. Template licence: MIT.
Related templates
Ad Click Aggregation System Design Template (Reference Design)
45-minute path, click dedupe, event-time windows, exactly-once counting, exact and Count-Min top-K, batch reconciliation for billing, failure modes, 10x row
System DesignZanzibar-Style Authorization Service System Design Template
Relationship-based authorization service: relation tuples, namespace config, Check/Expand/List/Watch APIs, consistency tokens, caching, sharding, failure modes
System DesignCDC Pipeline System Design Template
CDC pipeline: log-based capture, watermark chunked snapshot, per-key order, schema evolution, outbox, search, cache and warehouse sinks, replay