Infrastructure
Design Metrics & Monitoring
01
What the platform must do
What the platform must do
- Collect counters, gauges and histograms from hosts, containers and applications, each sample identified by a metric name plus labels such as
service,regionandstatus. - Query with a functional language: rates over windows, sums grouped by label, percentiles from histograms.
- Dashboards that refresh every 15 to 30 seconds.
- Alert rules evaluated continuously, routed to paging, chat or email with deduplication and silencing.
- Raw resolution for 15 days, downsampled data for 13 months.
- Multiple teams share the platform, each with its own quota.
- A sample becomes queryable within about 30 seconds of being produced.
- Alerts fire within 60 seconds of a condition becoming true.
- Dashboard queries over the last hour return in under 1 second at p95.
- The write path keeps working during partial outages; losing a few seconds of samples is tolerable, losing an hour is not.
- The monitoring system must not share failure domains with what it monitors.
Host count is the number people quote, but the cost driver is the number of unique series, every distinct combination of metric name and labels. Ask how many there are, how fast they churn (containers come and go), and whether anyone is allowed to use user IDs or request IDs as labels. The answer to the last one should be no, and you should say so.
02
Sizing the load
Sizing the load
Assume 400,000 monitored hosts and containers, each exposing about 1,000 series, collected every 10 seconds. Container churn replaces a third of all series every day.
| Quantity | Working | Result |
|---|---|---|
| Active series | 400K targets x 1,000 series | 400M |
| Ingest rate | 400M / 10 s interval | 40M samples / s |
| Raw sample size | 8 B timestamp + 8 B float | 16 B uncompressed |
| Compressed sample size | delta-of-delta timestamps, XOR floats | about 1.4 B |
| Raw data per day | 40M x 86,400 x 1.4 B | about 4.8 TB |
| Raw retention | 15 days x 4.8 TB x 2 replicas | about 145 TB |
| Downsampled retention | 5 min rollups, 5 aggregates, 13 months | about 60 TB in object storage |
| Series churn | one third of 400M per day | about 130M new series / day |
| Index memory | 400M series x about 1 KB of labels and postings | about 400 GB across the cluster |
| Alert rules | 60K rules evaluated every 30 s | 2,000 rule queries / s |
Without compression, one day of raw samples is about 55 TB. With time series encoding it drops to under 5 TB. A good encoding is the difference between a dozen storage nodes and a hundred, which is why the storage format gets most of the deep dive.
The sample data compresses well, but the inverted index over labels does not. With heavy churn it is often the first thing to run out of memory, long before disks fill.
03
API design
API design
04
Architecture overview
Architecture overview
flowchart LR
T["Hosts and apps"] --> AG["Local collector agent"]
AG --> GW["Ingest gateway"]
GW --> K[["Kafka: samples by series hash"]]
K --> IG["Ingesters: in-memory head blocks"]
IG --> WAL[("Local WAL")]
IG --> OBJ[("Object store: compacted blocks")]
CMP["Compactor and downsampler"] --> OBJ
Q["Query frontend"] --> QC[("Results cache")]
Q --> IG
Q --> SG["Store gateways"]
SG --> OBJ
R["Rule evaluator"] --> Q
R --> AM["Alert manager"]
AM --> N["Pager, chat, email"]A small agent on each host scrapes local endpoints every 10 seconds, adds host labels and ships batches to an ingest gateway. Pushing from an agent rather than pulling from a central server means the platform does not need network reach into every subnet, and the agent can buffer through a short outage.
The gateway authenticates the tenant, enforces quotas and writes batches into Kafka, partitioned by a hash of the series labels so all samples for one series land in order on one partition. Ingesters consume those partitions and keep the most recent two hours of each series in memory as compressed chunks, writing a local write-ahead log for crash recovery. Every two hours, an ingester cuts its in-memory data into an immutable block and uploads it to object storage.
Queries go through a frontend that splits long ranges by day, serves repeated pieces from a cache, and fans out to two kinds of readers: ingesters for the most recent hours, and store gateways that read blocks from object storage for everything older. A compactor merges small blocks into large ones and produces downsampled versions. The rule evaluator is just another query client that runs every alert expression on a schedule and sends firing alerts to an alert manager, which deduplicates, groups and routes them.
If ingesters slow down or restart, samples wait in Kafka instead of being dropped at the edge. It also lets you replay a partition into a fresh ingester after a crash.
The only stateful hot tier. Each owns a set of partitions, holds two hours of data in memory and is replicated twice so a single failure does not leave a hole in recent graphs.
Long-term blocks are immutable files, so cheap storage with high durability fits perfectly. Readers cache block indexes and hot chunks on local disk.
Rule evaluation reads the same data as dashboards. The alert manager handles what humans care about: grouping, silences, inhibition and escalation.
05
Deep dive: storage, the label index and cardinality
Deep dive: storage, the label index and cardinality
How do you store tens of millions of samples a second in a few bytes each? How do you find the right 800 series out of 400 million in milliseconds? And how do you stop one bad label from taking the whole platform down? The storage format, the index and the cardinality guards answer them in that order.
Compressing samples
Time series data is extremely regular, and the encoding exploits that. Timestamps for a series arrive at nearly fixed intervals, so store the first timestamp, then the first delta, then only the difference between consecutive deltas. For a perfectly steady 10-second scrape that difference is zero and costs a single bit. Small jitter fits in a handful of bits.
Values change slowly too. XOR each float with the previous one: identical values give zero (one bit), and similar values share their sign, exponent and leading mantissa bits, so you store only the meaningful middle bits plus a short header. This scheme, popularised by Facebook's Gorilla paper, brings a 16-byte sample to roughly 1.3 to 1.5 bytes on typical infrastructure data.
Samples are grouped into chunks of about 120 points per series. A chunk is the unit you compress, cache and read, which means a query for one hour of one series touches only a few chunks.
flowchart TD
S["Sample arrives at ingester"] --> H["Append to head chunk for its series"]
H --> W["Append to write-ahead log"]
H --> F{"Chunk has 120 points?"}
F -->|yes| C["Seal chunk, start a new one"]
F -->|no| H
C --> B{"Two hours elapsed?"}
B -->|yes| BL["Write block: chunks, label index, metadata"]
BL --> UP["Upload block to object store"]
UP --> CP["Compactor merges blocks into 1 day and 2 weeks"]
CP --> DS["Downsample to 5 min and 1 h rollups"]Finding series fast
Every block carries an inverted index: for each label pair such as service="checkout", a sorted list of the series IDs that have it. A query selector is a set of label matchers, so the engine fetches each matching postings list and intersects them. Because the lists are sorted, intersection is a linear merge. Regex matchers expand to the union of the matching label values first. The index also stores label names and values on their own, which is what dashboard dropdowns query.
The most expensive pattern is a matcher that hits millions of series, such as every pod of every service. Query planning should apply the most selective matcher first and refuse queries that would scan more than a set limit of series, returning a clear error instead of dragging the cluster down.
Cardinality is the real capacity limit
Each new label value creates a new series, and each series costs memory for its head chunk and index entry whether it receives one sample or a million. A single developer who adds user_id as a label to a request counter can create tens of millions of series in an hour. This is the most common way metrics platforms fall over.
Defend in layers. Per-tenant limits on active series, enforced at the gateway, reject new series once a tenant passes its quota while still accepting samples for existing ones. Per-metric limits catch a single runaway metric. Relabeling rules in the agent drop known bad labels before they leave the host. And a cardinality explorer shows teams which metrics and labels cost the most, so they can fix the cause.
Downsampling and long ranges
A 90-day graph at 10-second resolution would need almost 780,000 points per series, far more than a screen can show. The compactor writes rollups at 5 minutes and 1 hour, storing count, sum, min, max and last per window so that averages, rates and extremes can be computed correctly. The query engine picks the coarsest resolution that still gives enough points for the requested step.
Alert evaluation
Rules are sharded across evaluators by hash, and each one runs its rules on a fixed interval. A rule with a for clause only fires after the condition has been true for that long, which removes flapping from single bad scrapes. The evaluator also needs to alert on absence: a rule like "this service reported nothing for five minutes" catches a dead agent that would otherwise look like a quiet, healthy system.
"Agents push batches to a gateway that enforces series quotas, then Kafka partitions by series hash. Ingesters keep two hours in memory using delta-of-delta timestamps and XOR floats, about 1.4 bytes a sample, and flush immutable blocks with an inverted label index to object storage. Queries merge recent data from ingesters with older blocks, and long ranges read rollups. Alert rules run on the same query path, and the alerting stack is deployed separately from what it watches."
06
Mistakes to avoid
Mistakes to avoid
Every value becomes its own series with its own memory. A busy service can create millions of series an hour, and ingesters run out of memory for everyone.
A row per sample with a B-tree index costs tens of bytes per point and collapses under tens of millions of inserts a second.
When that cluster has a network or storage problem, both the application and its alerts go silent together.
Averages of percentiles are mathematically meaningless and hide the slow hosts you are looking for.
Storage grows linearly and year-long queries scan billions of points to draw a few hundred pixels.
07
Decisions and what they cost
Decisions and what they cost
Works across networks and accounts, buffers locally during outages, and gives a central place to enforce quotas. The cost is an agent on every host and less direct knowledge of which targets should exist.
Fits a multi-tenant platformThe pull model makes "target is down" a first-class signal and keeps applications simple. It needs service discovery and network reach to every target, which is harder across many accounts.
Great inside one clusterRecent data, which most queries want, is in memory. Old blocks are cheap to keep for a year. The price is a two-tier read path that has to merge and deduplicate results.
Cheap and fast where it mattersSimpler reads, but storage and compute scale together, so keeping a year means paying for a year of SSD and rebalancing terabytes when nodes change.
Expensive at this sizeWhen ingesters are overloaded, the gateway sheds load per tenant rather than slowing agents. A few seconds of missing data is visible but harmless.
Availability firstMonitoring data loses value quickly, and the coordination needed for exactly-once would slow the write path for a guarantee nobody uses.
Wrong guaranteeBucket counts add across hosts, so you can compute any percentile for any group afterwards. Precision depends on bucket boundaries, which must be picked per metric.
MergeableExact percentiles per process with no bucket tuning, but they cannot be combined across processes. Useful for a single service, not for fleet views.
Narrow use08
How to handle the interview
How to handle the interview
Turn hosts into series, then samples per second. Do this arithmetic first. It sets the scale for every other choice and shows you know series count is the real unit.
Explain the compression trick in two sentences. Delta-of-delta for timestamps, XOR for values, about 1.4 bytes a sample. Then show what that does to the storage bill.
Bring up cardinality before you are asked. Describe the user_id label incident and the quota that prevents it.
Separate hot and cold storage. Show the read path merging ingesters with object storage blocks, and explain downsampling for long ranges.
Ask who monitors the monitor. A separate alerting stack and a dead man's switch heartbeat show operational maturity.
Know why averaged percentiles are wrong. Interviewers often test this. Answer with mergeable histograms.
Talk about alert quality. A for duration, grouping and routing prevent pager fatigue. A design that pages on every blip is not finished.
09
Failure modes
Failure modes
A slow disk or a bad deploy makes consumers lag by fifteen minutes. Dashboards show flat lines and alerts evaluate on stale data, so they stay quiet during a real outage.
Alert on consumer lag directly from a separate system, make rule evaluators skip and flag evaluations when data is older than a threshold, and scale ingesters on lag.A new release labels a metric with the full request path including IDs. Active series double in twenty minutes and ingester memory hits its limit.
Per-tenant and per-metric series limits at the gateway reject new series past the quota, while existing ones keep flowing. The tenant gets a clear error and a pointer to the offending metric.An evaluator crashes in a loop. No alerts fire, which looks exactly like everything being healthy.
Have the alerting pipeline emit a constant "always firing" heartbeat to an external service that pages if the heartbeat stops arriving.Someone opens a dashboard with a year range and no filters. Store gateways load thousands of blocks and memory pressure slows every other query.
Enforce limits on series scanned and bytes fetched per query, use downsampled data for long ranges, split queries by day in the frontend, and give each tenant its own concurrency budget.A network partition heals and 400,000 agents send their buffered samples together, several times the normal rate.
Agents retry with jittered backoff and send buffered data at a capped rate. The gateway returns 429 with Retry-After instead of falling over.Compaction fails for days. Object storage fills with thousands of small blocks and long queries slow down because each must open many files.
Alert on the number of uncompacted blocks per tenant and on compactor progress, and make compaction jobs restartable and idempotent.10
Growing the design in stages
Growing the design in stages
One server scrapes everything, stores two weeks on local disk and runs the alert rules. Fast, simple and entirely sufficient at this size.
Scaling by splitting works for a while, but cross-cluster views need federation and nobody can see the whole fleet in one query.
Local servers upload their blocks to object storage, and a query layer reads across them for a global view and a year of history.
Agents push through a gateway and Kafka to a sharded ingester tier with replication, quotas and a query frontend. The single scraping server per team is retired.
Per-team cost reports, cardinality explorers and links from metrics to traces and logs turn the system into a product other engineers rely on daily.
Watch and read
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.

