InterviewKata
System design · Worked design D18 of 46 · Data and infrastructure · Long form

Design metrics monitoring and alerting

Every server, container and service emits numbers every few seconds: CPU, memory, request rates, error counts, latency percentiles. A monitoring system stores millions of these time series cheaply, draws them on dashboards in a blink, and wakes a person only when something is really wrong. The hard parts are the write volume, the storage format, cardinality and alerts that are neither silent nor noisy. It follows the interview framework phase by phase.

1. The prompt

"Design a metrics monitoring and alerting system" (think Prometheus, Datadog, Grafana Mimir, CloudWatch): collect metrics from a large fleet, let engineers query and graph them, and notify on-call engineers when rules are broken.

It is the most write-heavy design in the track: data arrives every second of every day, is almost never updated, and is mostly read for the last few hours. That shape drives a special-purpose storage engine.

2. Frame the problem

How big is the fleet?

About 100,000 hosts, ~500 series each.

50M active series: the write volume and memory budget come from this.

How often are samples taken?

Every 10 seconds.

5M samples a second, nonstop.

How long do we keep data?

Full resolution for two weeks, downsampled for a year.

Hot local storage plus cheap object storage.

Logs and traces too?

No, metrics only.

Numbers with labels: a time-series database, not a search index.

Multi-tenant?

Yes, many teams share it.

Per-tenant limits on series, or one team's mistake hurts everyone.

How fast must an alert fire?

Within a couple of minutes of a real problem.

Rule evaluation every 15–60 s with a short "for" duration.

3. What it must do

  1. Collect numeric metrics with labels (http_requests_total{service="checkout", status="500"}).
  2. Query and graph them: rates, sums by label, percentiles over any time range.
  3. Evaluate alert rules and notify the right people through pager, chat or email.

Not now:

4. How well it must do it

TargetWhy this number
Write-heavy: 5M samples a second, nonstop load shapeThe fleet never stops reporting.
Fast recent queries: dashboards over the last few hours in under a second speedEngineers debug incidents in real time.
Reliable alerting, even when what it monitors is down availabilityThe worst failure is a silent outage.
Cheap retention: a year, downsampled costCapacity planning and year-over-year comparisons.

5. Numbers that change the design

QuantityWorking→ so we…
Active series100,000 hosts × ~500 series each50M active time series → so shard by series
Ingest50M series ÷ one sample every 10 s5M samples a second → so sharded ingestion, append-only writes
Raw storage16 bytes a sample (8 B time + 8 B value) × 5M × 86,400~6.9 TB a day uncompressed → so compression is essential
CompressedGorilla-style encoding: ~1.37 bytes a sample~590 GB a day → so hot data on local disk, older blocks in object storage
Year-long graphOne series × 3.15M raw points→ so downsample to 1-hour points (8,760) for long ranges

6. The nouns

series id   hash(name + sorted labels) → {__name__="http_latency_bucket", service="checkout", le="0.5"}
samples     series id → [(t, v), (t, v), …]      in-memory chunk + WAL, then immutable 2-hour blocks
label index label=value → posting list of series ids
rule        name, expr, for, labels {severity, team}, annotations

7. The contract

GET  /metrics                         (each target exposes current values; the collector scrapes it)
POST /api/v1/write                    (or push batches of samples)
GET  /api/v1/query_range?query=histogram_quantile(0.99, sum by (le) (rate(http_latency_bucket[5m])))&start=…&end=…&step=60
rule: checkout_p99_high   expr: p99_latency_ms > 400   for: 5m   labels: severity=page, team=payments

Data flow: (1) collectors scrape or receive samples near the workloads → (2) forward batches to TSDB shards by series hash → (3) shards append to memory and a WAL, cut blocks every two hours → (4) old blocks go to object storage, compacted and downsampled → (5) the query service fans out to shards and storage → (6) a rule evaluator runs queries on a schedule and sends alerts to an alert manager.

8. First working version

Services write every measurement into one relational table (metric, labels, time, value); dashboards and a cron job query it.

8.1 Writing samples

8.2 Graphing

A dashboard runs a range query on the table with a B-tree index on (metric, time).

8.3 Alerting

A cron job runs each rule's query every minute and emails if it's true.

Parked for later (section 9 fixes each)

  1. 5M row inserts a second into one table, and every service needs database credentials.
  2. 16+ bytes a sample plus index overhead: tens of TB a day.
  3. Unbounded labels can create millions of series overnight.
  4. One-minute spikes page someone at 3 a.m.; a real outage sends 500 emails.
  5. If the database goes down, alerting goes down with it.

9. Make it hold up

9.1 Pull or push? write-heavy

ApproachWhy
✗ BadEvery service writes each measurement straight into a central databaseMillions of tiny writes, every service needs database credentials, and an overloaded database slows the services themselves.
~ GoodPull (Prometheus scrapes /metrics) or push (agents or OpenTelemetry collectors send batches)Pull makes "is the target up?" free and keeps targets simple; push suits short-lived jobs, serverless and networks the collector can't reach.
✓ GreatCollectors near the workloads (per cluster or region) that scrape or receive, batch and forward to a sharded central storeLocal collection keeps working during network trouble, batching cuts overhead, and the central store scales by adding shards.

9.2 The time-series database write-heavy · cheap

9.3 Long-term storage and downsampling cheap retention

Blocks are uploaded to object storage, compacted into larger blocks and downsampled (5-minute and 1-hour resolution) so a year-long graph reads few points. Thanos, Grafana Mimir and VictoriaMetrics add this to the Prometheus model. The query layer picks the coarsest resolution that still gives enough points for the graph's width.

9.4 Cardinality: the silent killer write-heavy · cost

Every distinct combination of label values is a separate series. Add a user_id label to a latency metric with 10M users and you create 10M series overnight, exhausting memory. Keep labels bounded (service, endpoint, status class, region), put per-user detail in logs or traces, and enforce per-tenant series limits at ingestion: reject or drop new series past the limit, and tell the team which metric grew.

9.5 Alerting that people trust reliable alerting

9.6 Monitoring the monitor reliable alerting

Run rule evaluation and the alert manager in pairs, close to the data, and independent of the systems they watch. A separate, simple watchdog alerts if the monitoring system itself stops working: a "dead man's switch" alert that must always be firing, so its absence pages someone through an outside service.

9.7 The whole design after the deep dives

Samples from collection to an alert. Press Play or drag the timeline.

10. Interviewer follow-ups

How do you compute a p99 across 200 servers?

Not by averaging per-server p99s. Each server exports a histogram (counts per latency bucket); sum the buckets across servers, then compute the quantile from the combined histogram.

A dashboard query is slow.

Recording rules precompute expensive expressions every minute into new series; the dashboard reads those. Also use a coarser step or resolution for long ranges.

One team creates 5M series by mistake.

Per-tenant series limits reject the excess at ingestion; the team gets an alert naming the metric and label. Other tenants are unaffected.

Global view across 20 regions.

Keep collection and alerting regional; a global query layer fans out to regional stores (or a central store receives copies) for cross-region dashboards.

A host stops sending metrics.

With pull, the scrape fails and up == 0 is a metric you can alert on. With push, alert on "no samples for N minutes" (absent()).

Late or out-of-order samples.

Accept within a small window into the in-memory chunk; reject older ones, or allow a configurable out-of-order window at the cost of more memory.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
TSDB shard downGaps in some graphsShard healthReplica; WAL replay on restart
Collector can't reach storeDelayed dataRemote-write queue sizeLocal buffer; regional alerting keeps working
Cardinality explosionSlow queries, memory alarmsSeries count per tenantPer-tenant limits
Alert manager downNo pagesDead man's switchPair of alert managers; outside watchdog
Alert stormPager floodNotification rateGrouping, inhibition rules

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch:

At 10×:

A 45-minute interview

0–4Frame the problem (fleet, interval, retention)
4–9Must do, how well, numbers (5M samples/s, 590 GB/day)
9–12Nouns, contract, data flow
12–18First working version, parked list
18–40Deep dives: collection (9.1), the TSDB (9.2), cardinality (9.4), alerting (9.5)
40–45Follow-ups and wrap-up

A 60-minute interview

0–5Frame the problem
5–11Must do, how well, numbers
11–15Nouns, contract, data flow
15–23First working version
23–50Deep dives 9.1–9.6 and the final picture
50–60Follow-ups: global p99, slow dashboards, global view; wrap-up
LevelWhat a strong answer shows
Mid-levelAgents send metrics to a collector and a time-series database; dashboards query it; rules send alerts.
SeniorPull vs push, write-volume estimate, append-only chunks and compression, label index, cardinality limits, downsampling, for durations and an alert manager that groups and routes.
Staff+Multi-tenant sharding and limits, global query over regional stores, meta-monitoring, SLO-based alerting policy, cost per series, and making the monitoring system survive the outages it reports.

Common mistakes:

13. Try it, check yourself, read more

Six hours of checkout p99 latency, one point a minute. A real incident starts at 3:50 and lasts half an hour; the rest has noise and a few one-minute spikes. Tune the rule so it pages for the incident quickly but not for the spikes.

Why is adding a user_id label to a latency metric dangerous?

Each label combination is its own series; unbounded labels explode memory and cost.

Why does time-series data compress so well?

Delta-of-delta timestamps and XOR-ed values are mostly zeros.

What does for: 5m on an alert rule do?

Until then the alert is pending, which filters short spikes.

How do you get the fleet-wide p99 latency?

Percentiles don't average; histograms add.
ResourceWhy
Gorilla: a fast, scalable, in-memory time series database (VLDB 2015)The compression scheme most time-series databases use.
Alerting rules (Prometheus docs)Pending and firing states, for and keep_firing_for.
Monitoring distributed systems (Google SRE book)Symptoms vs causes and the four golden signals.