Vibe Engines
YouTube
System Design

Design a Metrics & Monitoring System

Step 1 / 9

Learn system design by building a metrics and monitoring system like Prometheus, Datadog or Grafana step by step.

The numbers to beatpullcollector-pacedscrape fail= target downpush gwephemeral jobs

The whole design, in writing

Learn system design by building a metrics and monitoring system like Prometheus, Datadog or Grafana step by step. An interactive guide covering why SSH-and-grep can't scale, instrumentation and metric types, pull vs push collection, storing metrics in a time-series database, alerting rules with dedupe and routing, dashboards, high-availability collectors and federation, and the unhappy paths (alert storms, cardinality, monitoring the monitor).

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What is a monitoring system?

You run thousands of services across thousands of machines. When something breaks at 3am — latency spikes, a disk fills, error rate climbs — you need to know before your users do, see where it's happening, and get paged. SSHing into boxes to tail logs doesn't scale past a handful of servers.

Targets/metrics
New in this step: Targets.

A monitoring system turns a fleet's raw signals into awareness. It collects metrics from every target, stores them in a time-series database, lets humans query and visualize them, and — most importantly — alerts a human when something crosses a line. Collect → store → query → alert.

What the new pieces do

Targetsclient
The services, hosts and containers you're watching. Each exposes its current metrics at an endpoint (or ships them out).

Step 1 · The reactive trap

SSH-and-grep doesn't scale

The starting point for many teams: when something seems wrong, log into servers and check. It works for three servers. Why is it hopeless — and dangerous — for three thousand?

Targets/metrics
The system as it stands at this step.

Manually checking servers when something "seems wrong". Why does it fail at scale?

  1. Bandwidth isn't the issue. The problems are that it's reactive (you look only after noticing), unscalable (you can't watch thousands of boxes by hand), and has no history to compare against.

  2. Log size is a symptom. The core failure is that manual, reactive inspection can't continuously watch a fleet or catch problems before users do — you need automated, always-on measurement.

  3. You only look after something breaks, you can't manually watch thousands of hosts, and you have no baseline to compare "now" against. Monitoring must be automatic, continuous, historical, and page you proactively.

Manual inspection is reactive (you look only after noticing), unscalable (no human watches thousands of hosts), and historyless (no baseline for "is this normal?"). Monitoring flips all three: continuously measure everything, store the history, and automatically alert so the system tells you — before the users do.

Why this piece earns its place

Worth saying out loud what continuous measurement gives up. SSH-and-grep was open-ended: at three in the morning you could look at anything on that box, including the thing nobody thought to record. A metrics pipeline only ever sees what was instrumented in advance, so the first incident caused by something you never emitted is invisible on every dashboard you own — and the graphs will look calm while the service burns. That isn’t an argument for keeping the manual path as the primary one; it’s an argument for two habits. Keep an exploratory escape hatch — logs, traces, and the ability to get onto a host — for the questions metrics structurally cannot answer. And treat every incident the metrics missed as an instrumentation bug, with a fix that ships alongside the code fix. The pipeline improves only if someone deliberately closes the gaps each incident reveals; otherwise you’ve automated exactly the visibility you had a year ago, and quietly stopped looking anywhere else.

Step 2 · What to measure

Instrumentation & metric types

Before collecting anything, decide what a "metric" even is. You want cheap-to-store numbers that summarize behavior — not every event. What primitive metric types cover what you need to know?

Targets/metrics
The system as it stands at this step.

What form should the measurements take?

  1. Storing and counting every event is the log/event pipeline's job — far more expensive and detailed than you need for health. Metrics are cheap pre-aggregated numbers: counters, gauges, histograms.

  2. A counter only goes up (requests, errors); a gauge goes up and down (memory, queue depth, temperature); a histogram buckets a distribution (request latency) so you can compute p50/p99. These few primitives, sampled over time, describe system health compactly.

  3. One boolean throws away all the signal you need to see trends, distributions and leading indicators. You want quantitative metrics over time, not a binary.

Instrument code to emit a few numeric metric types: a counter (monotonic — requests, errors), a gauge (up/down — memory, queue depth, temperature), and a histogram/summary (a bucketed distribution — request latency, so you can compute p50/p99). Each metric carries labels (service, region, endpoint) for slicing. Cheap numbers sampled over time, not raw events.

Why this piece earns its place

Pre-aggregation is the whole win here, and it’s also irreversible. A histogram’s buckets and a metric’s labels are chosen at emit time, in code, and you can’t recover a distribution you never bucketed — if the top latency bucket ends below where your real tail lives, every slow request lands in one indistinguishable lump, and the day p99 matters you’re shipping a code change and waiting on a deploy to see it. It cuts both ways: each extra bucket multiplies the series this metric produces, on every instance, forever. The subtler trap is arithmetic. Percentiles don’t average. The mean of each host’s p99 is not the fleet’s p99, it isn’t any meaningful number at all, which is why latency has to be emitted as bucket counts that get summed across hosts before the quantile is computed — a summary that pre-computes quantiles per process can’t be aggregated afterwards. So pick buckets around the thresholds you intend to alert on, and accept that changing them is a deploy, not a query.

Step 3 · Getting the numbers

Pull vs push collection

Metrics live inside thousands of processes. Something has to gather them centrally. Should the central system pull (scrape targets on a schedule) or should targets push their metrics out? This choice shapes the whole system.

Push Gatewaybatch jobsCollectorscrape · pull
New in this step: Push Gateway, Collector.

How does the central system get metrics off thousands of processes?

  1. Push works and suits some cases (short-lived jobs, serverless), but pure push makes it hard to know if a target is down (silence vs. never-existed) and lets a misbehaving client flood you. Pull gives the collector control and free liveness detection.

  2. Manual export is the reactive trap again. Collection must be automatic and frequent — every few seconds — via pull or push, not a person.

  3. In the pull model each target exposes its current metrics at an endpoint and the collector scrapes them every N seconds. Pull gives the collector control over rate, makes "target down" obvious (scrape fails), and simplifies discovery. Short-lived batch jobs that vanish between scrapes push to a gateway that the collector then scrapes.

Favor a pull model (like Prometheus): each target exposes its current metrics at a /metrics endpoint, and the collector scrapes it every few seconds. Pull gives the collector control of the sampling rate, makes a down target obvious (the scrape simply fails), and pairs naturally with service discovery. For short-lived batch jobs that die between scrapes, add a push gateway they push to and the collector scrapes.

Why this piece earns its place

The knob this step hands you is the scrape interval — this page picks fifteen seconds — and it isn’t a free parameter. Shorten it and every target pays more CPU serialising its metrics, the collector does proportionally more work, and the sample firehose the next step has to store grows in step with the change. Lengthen it and two things bite. Anything shorter than the interval simply isn’t sampled: a gauge like queue depth can spike and fully recover between two scrapes and leave no trace, which is a good reason to reach for a counter for anything you genuinely need counted, since a counter carries the whole delta no matter when you read it. And the interval sets the floor on detection — no alert can know about a condition sooner than the next successful scrape — so it has to stay small relative to how long you make alerts hold before firing, or the two compose into a delay nobody budgeted for. Set it per job rather than globally: a busy front end and a nightly batch host don’t deserve the same rate.

  • pullcollector-paced
  • scrape fail= target down
  • push gwephemeral jobs

What the new pieces do

Push Gatewayservice
A catch for short-lived jobs that can't be scraped: they push their metrics here, and the collector scrapes the gateway.
Collectorgateway
Periodically pulls metrics from every target, adds labels, and writes the samples into the time-series store.

Step 4 · Where it all lives

Store it in a TSDB

Scraping thousands of targets every few seconds produces a firehose of timestamped samples — millions of points a second, kept for months, queried by time range. A general database can't hold this. Where do the samples go?

Collectorscrape · pullTime-Series DBstore + query
New in this step: Time-Series DB.

Millions of samples/sec, kept for months, queried by range. Where do they live?

  1. A row-per-sample table melts on write amplification, index bloat and slow range scans at this volume — exactly the problem a specialized store exists to solve.

  2. The TSDB is purpose-built for this: append-only ingest, columnar compression (~1–2 bytes/point), time partitioning, downsampling and retention. Collectors write samples in; queries aggregate ranges out.

  3. Months of millions-per-second samples can't live in RAM, and it wouldn't survive a restart. Recent data sits in memory but is flushed durably to the compressed on-disk TSDB.

Store samples in a time-series database — the specialized engine for append-heavy, range-queried numeric data. Collectors write samples in; dashboards and alert rules query ranges out. The TSDB handles the hard parts (columnar compression to ~1–2 bytes/point, LSM ingest, downsampling, retention) so the rest of the pipeline just reads and writes it.

Why this piece earns its place

The assumption this store quietly bakes in is that samples arrive in time order. Append-only ingest into a writable head block is what makes the compression and the time partitioning work, and the price is that a sample older than what the head already holds is typically refused, not stored. That happens more often than it sounds: a target that was unreachable and now exposes catch-up values, a federated scrape pulling data that is already minutes old, and above all clock skew — a host whose clock runs fast writes points from the future, then appears to travel backwards the moment it’s corrected. The defence is mostly discipline. Timestamp at scrape time using the collector’s clock rather than trusting each target’s, keep the collectors’ own clocks disciplined, and — the part people skip — expose the count of rejected and out-of-order samples as a metric and alert on it. Otherwise the loss is silent: the write is dropped, the query still succeeds, and the resulting gap looks exactly like a period when nothing happened.

  • appendsample firehose
  • ~1–2 Bper point
  • rangeaggregation reads

What the new pieces do

Time-Series DBdata
The specialized store for all the samples: append-heavy writes, range-aggregation reads. The heart of the system.

Step 5 · The whole point

Alerting: rules, dedupe & routing

Data sitting in a database doesn't wake anyone up. The real job is: when error rate exceeds a threshold for 5 minutes, get the right person paged — once, not a thousand times, and not for a blip. How?

Rule Evaluatoralert rulesTime-Series DBstore + queryAlert Managerdedupe · routeNotifypage · Slack
New in this step: Rule Evaluator, Alert Manager, Notify.

How do you turn "error rate high for 5 min" into exactly the right page?

  1. Alerting on raw points floods on-call with noise and false pages from momentary blips. You need a rule that must hold FOR a duration, and a layer that dedupes/groups before notifying.

  2. Humans staring at dashboards is the reactive trap and can't cover 24/7. Alerts must be automatic, evaluated continuously against rules.

  3. A rule like "error_rate > 5% FOR 5m" is evaluated on a schedule; when it holds it fires an alert. A separate alert manager deduplicates identical alerts, groups related ones, honors silences/maintenance windows, and routes to the right on-call via the right channel — one clear page, not a storm.

Split it in two. A rule evaluator runs alerting queries against the TSDB on a schedule; an alert fires only when a condition holds FOR a duration (so blips don't page). A separate alert manager then deduplicates identical alerts, groups related ones, honors silences/maintenance windows, and routes each to the right on-call via the right channel (page, Slack, ticket). Signal in, one clear page out.

Why this piece earns its place

The FOR duration is the knob, and five minutes is a choice, not a default. Too short and you’re back to paging on blips. Too long and you’ve written a fixed delay into every incident that rule covers — the condition is true, users are already affected, and the page is being deliberately withheld. The failure that surprises people sits between the two: a rule whose condition flaps around the threshold may never fire at all, because each dip back under resets the timer, so a service oscillating in and out of trouble stays invisible for as long as it never fails continuously. That’s an argument about what goes in the expression, not just how long you wait. Alerting on a ratio computed over a window — the share of requests failing over the last while — integrates the flapping instead of resetting on it, and crosses the line faster the worse the incident is, which is the behaviour you actually want from a pager. Keep instantaneous thresholds with short FOR durations for conditions that can’t flap, like a disk that only fills in one direction.

  • FOR 5mno blip pages
  • dedupe+groupone page, not 500
  • routeright on-call

What the new pieces do

Rule Evaluatorservice
Runs alerting queries against the TSDB on a schedule; when a condition holds for long enough, it fires an alert.
Alert Managerservice
Deduplicates, groups, silences and routes firing alerts to the right people through the right channels.
Notifynotify
The channels an alert lands in: pager/on-call, chat, email, tickets. The last mile from signal to human.

Step 6 · Make it visible

Dashboards & queries

Humans need to see trends, compare "now" to last week, and drill into an incident across dimensions. Alerts tell you something is wrong; how do people investigate?

Rule Evaluatoralert rulesTime-Series DBstore + queryDashboardsquery · visualize
New in this step: Dashboards.

Add a query language over the TSDB (PromQL-style) and dashboards (Grafana-style) that visualize it: time-series panels, heatmaps, SLO burn-down, and label-based drill-down. A good dashboard turns "the site is slow" into "p99 latency for the checkout service in eu-west jumped when deploy X shipped." Alerts detect; dashboards diagnose.

Why this piece earns its place

Dashboards read from the same TSDB the rule evaluator reads from, and they read badly: a wall-mounted panel refreshing a month-long range across every label is one of the heaviest queries this system will ever run, and it runs forever whether or not anyone is looking at it. During an incident that load goes up — every engineer opens every dashboard and widens the range — and what those queries compete with is alert evaluation. A monitoring system that stops evaluating rules because humans are investigating has inverted its own priorities at the worst possible moment. So give the two read paths separate budgets: query limits and timeouts a dashboard hits well before the evaluator does, and, where you can, a separate read replica for interactive queries. Then stop recomputing the expensive expressions at all. A recording rule evaluates a costly aggregation once on a schedule and writes the result back as its own series, so both the dashboard and the alert read a cheap precomputed number — and they keep agreeing with each other, which is the other half of why the same expression should back both.

What the new pieces do

Dashboardsservice
Grafana-style panels that query the TSDB to visualize trends, SLOs and system health for humans.

Step 7 · Watch the whole fleet

Scale: HA collectors & federation

One collector can't scrape a whole datacenter, and if your monitoring has a single point of failure it will blind you exactly when an incident takes that node down. How do you scale collection and keep it reliable?

TargetsPush GatewayCollectorRule EvaluatorTime-Series DBAlert ManagerDashboardsNotify
The system as it stands at this step. · swipe to pan the diagram

Run collectors in redundant pairs (both scrape the same targets) for high availability, and shard scraping so each collector owns a slice of targets (by service/region), discovered via service discovery. Federate: local collectors keep full-resolution data, and a global layer scrapes aggregated rollups from them for a fleet-wide view. Cluster the alert manager so alerts survive a node loss. And keep cardinality in check — labels with unbounded values (user/request ids) will blow up the TSDB.

Why this piece earns its place

Two collectors scraping the same targets do not produce one dataset. They scrape at different instants, so the two copies of a series disagree sample by sample — you can’t merge them and shouldn’t try. The usual arrangement is to let both replicas run the same rules independently and lean on the alert manager’s deduplication to collapse the two identical alerts into a single page, which is a second reason that component earns its place, and to pin dashboards to one replica at query time rather than blending them, accepting that a panel’s data shifts slightly when the chosen replica changes. Federation has its own edge: it is itself a scrape, with a timeout, and a scrape that doesn’t finish in time yields nothing — not a partial result. A global layer pointed at a raw regional endpoint works right up until that region grows, then fails wholesale and takes your fleet-wide view with it. So federate from recording-rule output only, keep the federated series count deliberately small, and stay honest about what you bought: the global layer can tell you a region is unhealthy, and only the regional collector can tell you which instance.

Step 8 · The sharp edges

Alert storms & monitoring the monitor

Monitoring's own failure modes are brutal: one root cause fires a thousand alerts (an alert storm), a metric's cardinality explodes, and — worst — the monitoring system itself goes down silently and you never find out.

TargetsPush GatewayCollectorRule EvaluatorTime-Series DBAlert ManagerDashboardsNotify
The system as it stands at this step. · swipe to pan the diagram

Tame alert storms with grouping, inhibition (a "datacenter down" alert suppresses its 500 child alerts), and rate-limited routing. Enforce cardinality limits and reject/relabel high-cardinality labels. Set sane thresholds + FOR durations to cut false pages, and alert on symptoms (user-facing SLOs) over causes. Crucially, monitor the monitor: a separate, independent dead-man's switch that pages you if the pipeline stops producing data or the heartbeat alert stops firing.

Why this piece earns its place

Everything in this step shares one property that makes it uniquely dangerous: its failure mode is silence. An inhibition rule that has stopped matching, a silence that outlived the maintenance window it was written for, and a dead-man’s switch whose delivery path is broken all look identical to a quiet night. And the dependency model behind inhibition is hand-maintained — rename an alert or add a label and the parent stops suppressing its children, with no error anywhere, until the next datacenter event brings back the storm you thought you’d fixed. So two rules. The watchdog must not share fate with what it watches: if the dead-man’s switch is evaluated by your rule evaluator, routed by your alert manager and delivered by your paging integration, it is certifying the health of a pipeline it is part of, and a whole-region outage takes the alarm out along with everything else. Put its receiving end somewhere you don’t operate. And exercise all of this on purpose — fire a synthetic parent alert and check the children stay quiet, stop the heartbeat and check you get woken — because these are the only rules on the page that no incident will ever test for you.

You did it

You just designed a monitoring system.

TargetsPush GatewayCollectorRule EvaluatorTime-Series DBAlert ManagerDashboardsNotify
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • SSH-and-grep is reactive, unscalable and historyless — measure continuously and alert automatically.
  • Instrument with counters, gauges and histograms plus labels; watch the golden signals.
  • Prefer pull (scrape /metrics) for control + free liveness; add a push gateway for ephemeral jobs.
  • Store the sample firehose in a time-series database — the compressed, range-queried heart.
  • Split alerting: a rule evaluator fires (FOR a duration); an alert manager dedupes, groups, routes.
  • Dashboards + a query language turn detection into diagnosis across labels.
  • Scale with HA + sharded collectors and federation; cap cardinality; and monitor the monitor.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Pull vs push for metric collection — what's the real trade-off?

    In a pull model the monitoring system scrapes each target's metrics endpoint on its own schedule. That hands the collector control over sampling rate, gives free liveness detection (a failed scrape means the target is down, which is unambiguous), and pairs naturally with service discovery, since the collector already maintains the list of targets to scrape. Its weaknesses are ephemeral or unreachable targets: a batch job that lives for two seconds may never be scraped, and a target behind a NAT or firewall the collector can't reach won't work. In a push model targets send metrics outward on their own schedule, which handles short-lived jobs, serverless, and locked-down networks well — but it makes "is this target down or did it just never report?" ambiguous, and a misbehaving client can flood the ingestion endpoint. Most real systems are predominantly one model with an escape hatch for the other: Prometheus is pull-first with a push gateway for batch jobs; Datadog/StatsD are push-first via local agents.

  2. Why separate the rule evaluator from the alert manager?

    They solve two different problems, and coupling them makes both worse. Rule evaluation is about detection: on a schedule, run a query against the metrics ("error rate over 5% for the last 5 minutes") and decide whether a condition is true. Alert management is about notification hygiene: taking the raw firing alerts and turning them into exactly the right human signal. That second job includes deduplicating identical alerts coming from many replicas, grouping related alerts into a single notification, applying silences during known maintenance, inhibiting downstream alerts when a root-cause alert is already firing, and routing each alert to the correct on-call rotation through the correct channel. If detection and notification were one component, the same alert from 500 hosts would become 500 pages, you couldn't silence a service during a deploy without editing rules, and you couldn't route by team. Separating them means the rule side stays a pure function of the metrics, while the alert manager independently controls who gets woken up and how often — and it can be clustered for its own high availability.

  3. What is an alert storm and how do you prevent one?

    An alert storm is when a single underlying failure generates a flood of alerts that overwhelms on-call and buries the real signal — for example, a datacenter losing power fires "host down" for every one of its 500 machines, plus every service-level alert that depends on them. The antidotes are layered. Grouping collapses many similar alerts (same alertname, same cluster) into one notification instead of hundreds. Inhibition expresses dependencies so that a higher-level alert suppresses the alerts it obviously causes — if "datacenter unreachable" is firing, don't also page for each host in it. Rate-limiting and good routing prevent a channel from being flooded. And upstream, alerting on symptoms (user-facing SLOs like "checkout error rate is high") rather than on every cause reduces the raw number of rules that can fire. The goal is that one incident produces one actionable page, not a thousand notifications you'll learn to ignore.

  4. How do you monitor the monitoring system itself?

    This is the subtlest reliability requirement: the monitoring system must be more trustworthy than what it watches, because its cruelest failure mode is dying silently — the scrapers stop, no data flows, no alerts fire, and everything looks calm precisely because nothing is being measured. You defend against it several ways. Run the collectors in redundant pairs and the alert manager as a cluster so no single node is a blind spot. Federate so a global layer can observe that regional collectors are alive. And most importantly, deploy a dead-man's switch: a rule that is always firing under normal conditions, wired to an external, independent watchdog (often a third-party service) that pages you the moment that always-on alert stops arriving. In other words, instead of only alerting when something is wrong, you also continuously prove the pipeline is alive, and alert on the absence of that proof — so a dead monitor pages you from outside itself rather than failing quietly.

  5. Why is cardinality the dominant scaling constraint here too?

    Because this system stores its data in a time-series database, and a TSDB's memory and index cost scale with the number of distinct active series — every unique combination of metric name and label values is a separate series. In a monitoring context the labels come from instrumentation, so it's dangerously easy to attach a high-cardinality label (a user ID, request ID, full URL, or raw timestamp) to a metric and unintentionally create a new series for every distinct value, exploding series count into the millions and toppling the TSDB. The discipline is to keep labels bounded and low-cardinality (service, region, endpoint template, status code), enforce per-metric series limits, relabel or drop offending labels at ingestion, and push genuinely high-cardinality identifiers into logs or traces — which are built for that — rather than into metrics. Controlling cardinality is what keeps the whole monitoring pipeline affordable and fast.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. A monitoring system replaces SSH-and-grep because manual checking is…

    Monitoring measures continuously, keeps history, and pages you proactively.

  2. The core metric types are…

    Monotonic counters, up/down gauges, and bucketed histograms cover most health signals.

  3. A pull (scrape) model is favored because it gives…

    Pull controls rate, makes down targets obvious, and pairs with service discovery; push gw for jobs.

  4. Alerting is split into a rule evaluator and an alert manager so that…

    One firing condition from 500 hosts becomes one grouped, routed, silenceable page.

  5. The most dangerous failure of a monitoring system is…

    If the pipeline stops, all looks calm; an independent watchdog must page on missing heartbeats.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Instrument: emit counters, gauges and histograms with labels — the golden signals everywhere.
  • Collect: a collector pulls (scrapes) each target’s /metrics; a push gateway catches ephemeral jobs.
  • Store: write the sample firehose into a time-series database, queried by range.
  • Alert: a rule evaluator fires FOR a duration; an alert manager dedupes, groups, silences and routes.
  • Visualize + scale: dashboards query the TSDB; HA collectors + federation keep it reliable.

The qualities that shape everything

Each one names the mechanism that buys it.

Know before users do, automatically
Continuously measure everything, store the history, and alert automatically — flipping SSH-and-grep’s reactive, unscalable, historyless model.
Cheap summarized signals, not raw events
Instrument counters, gauges and histograms with labels (the golden signals) — pre-aggregated numbers sampled over time.
Collector-paced sampling and free liveness detection
A pull model scrapes each target’s /metrics on a schedule, making a down target obvious (the scrape fails) and pairing with service discovery; a push gateway catches ephemeral jobs.
Hold a sample firehose for months, queried by range
A time-series database — columnar, compressed to ~1–2 bytes/point, time-partitioned, append-only — is the store built for it.
One clear page, not five hundred
Split alerting: a rule evaluator fires only when a condition holds FOR a duration, and an alert manager dedupes, groups, silences and routes.
Monitoring more reliable than what it monitors
Run collectors in redundant pairs, shard scraping by service/region, federate a global view from regional collectors, and cluster the alert manager.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Automatic, continuous monitoring over SSH-and-grep when something seems wrong

Manual inspection is reactive, unscalable, and historyless — you look only after noticing and have no baseline; continuous measurement with automated alerting tells you before users do.

Counters, gauges, histograms + labels over storing every raw event and counting later

Storing every event is the log pipeline’s far-more-expensive job; cheap pre-aggregated numbers sampled over time describe health compactly, with labels to slice.

A pull (scrape) model over pure push from every target

Pure push makes "down vs never-existed" ambiguous and lets a client flood you; pulling gives the collector control of the rate, free liveness detection, and easy discovery — with a push gateway for ephemeral jobs.

A time-series database over a relational table, one row per sample

A row-per-sample table melts on write amplification and slow range scans at millions/sec; a TSDB is purpose-built for append-heavy ingest and range-aggregation reads.

Rule evaluator + alert manager over paging on every data point over threshold

Alerting on raw points floods on-call with blips; requiring a condition to hold FOR a duration and deduping/grouping/routing turns 500 hosts’ alerts into one clear page.

The answer, out loud

What a strong answer to “Design a Metrics & Monitoring System” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Frame the job first

    Let me frame what we’re building before I draw anything. The job is to know that something is wrong before a user tells us, across thousands of services on thousands of machines, and to say where. The baseline I’m replacing is someone logging into boxes when something seems off, and I want to name why that fails, because it shapes everything after: it’s reactive, so I only look once I’ve noticed; it doesn’t scale past a handful of hosts; and it leaves no history to compare today against. So the design goal is measure continuously, keep the history, and page automatically.

    Built in step 1: SSH-and-grep doesn't scale
  2. 3–8 min

    What counts as a measurement

    First question is what a measurement even is, and I’d push back on storing every event and counting them later — that’s a logging pipeline, and it’s far more expensive than answering is this thing healthy. I want a few cheap numeric types instead. A counter that only goes up, for requests and errors. A gauge that moves both ways, for memory or queue depth. And a histogram for latency, so I can get a p99 rather than an average that hides the tail. Every one of them carries labels — service, region, endpoint — because that’s how I slice things during an incident. For what to instrument I’d default to the golden signals: latency, traffic, errors, saturation.

    Built in step 2: Instrumentation & metric types
  3. 8–15 min

    Getting the numbers out

    Then collection, off thousands of processes. I’d go with pull: each target exposes its current metrics at an endpoint, and a collector scrapes it on a schedule. I’m choosing that mainly for two properties. The collector owns the sampling rate, so a misbehaving client can’t flood me. And a failed scrape is an unambiguous signal that the target is down — with pure push, silence and never-existed look exactly the same. Pull also pairs naturally with service discovery, since the collector is already maintaining the list of things to scrape. The obvious gap is short-lived jobs that die between scrapes, so I’d add a push gateway they write to, and have the collector scrape the gateway.

    Built in step 3: Pull vs push collection
  4. 15–21 min

    Where the samples live

    All of that lands in a time-series database, and I want to be explicit about why not a normal table with a row per sample. At millions of samples a second that dies on write amplification and index maintenance, and the reads I care about are range aggregations, which is the shape it’s worst at. A time-series store is append-heavy by design, keeps points columnar and compressed down to a byte or two each, partitions by time, and gives me retention and downsampling as policy rather than as jobs I write myself. Collectors write samples in, and everything downstream queries ranges out.

    Built in step 4: Store it in a TSDB
  5. 21–29 min

    Turning data into one page

    Now the part that actually matters, because data sitting in a database wakes nobody up. I’d split alerting in two. A rule evaluator runs queries against the store on a schedule and fires only when a condition has held for a duration — error rate over the threshold for five minutes, say — so a momentary blip never pages anyone. Then a separate alert manager takes the firing alerts and does the human-facing work: deduplicate identical ones, group related ones, honour silences during a deploy, and route each to the right on-call through the right channel. That split is the reason the same problem on five hundred hosts becomes one page instead of five hundred.

    Built in step 5: Alerting: rules, dedupe & routing
  6. 29–34 min

    From detection to diagnosis

    Alerts tell me something is wrong; they don’t tell me what. So I want a query language over the store and dashboards on top of it, and I’d insist the alert rules and the dashboard panels are written in the same expressions, so what we graph is what we page on. That’s what turns the site is slow into p99 latency for checkout in one region moved when a deploy shipped.

    Built in step 6: Dashboards & queries
  7. 34–39 min

    Scaling collection without a blind spot

    On scale, one collector can’t scrape a whole datacenter, and more importantly, monitoring with a single point of failure will blind me exactly when an incident takes that node out. So collectors run in redundant pairs scraping the same targets, sharded by service or region so each owns a slice, with service discovery feeding them their list. For a fleet-wide view I’d federate: regional collectors keep full resolution, and a global layer scrapes aggregated rollups from them. I’d cluster the alert manager too, so losing a node doesn’t cost me notifications. And I’d cap cardinality here, because a label with unbounded values will take the store down on its own.

    Built in step 7: Scale: HA collectors & federation
  8. 39–43 min

    The unhappy paths

    The failure modes I’d want to talk about are the monitoring system’s own. One root cause firing a thousand alerts is the common one, and I’d handle it with grouping, with inhibition so a datacenter-down alert suppresses the host alerts it obviously causes, and by alerting on user-facing symptoms rather than on every possible cause. The one that actually scares me is the monitoring dying quietly — nothing gets scraped, no rule fires, and everything looks calm precisely because nothing is being measured. That’s what a dead-man’s switch is for: a rule that is always firing in normal conditions, watched from outside the system, so it’s the absence of it that pages me.

    Built in step 8: Alert storms & monitoring the monitor
  9. 43–45 min

    Close on the trade-off

    To close: every choice here buys reliability with cost and complexity, and the honest limitation is that this system’s value is capped by what somebody remembered to instrument — it can only ever alert on questions we asked in advance. With more time I’d spend it on two things. Retention and downsampling policy, because that’s where the money actually goes. And tying metrics to traces and logs, so a spike on a graph gets me to the specific request that caused it instead of just the shape of the problem.

What this teaches

Learn system design by building a metrics and monitoring system like Prometheus, Datadog or Grafana step by step. An interactive guide covering why SSH-and-grep can't scale, instrumentation and metric types, pull vs push collection, storing metrics in a time-series database, alerting rules with dedupe and routing, dashboards, high-availability collectors and federation, and the unhappy paths (alert storms, cardinality, monitoring the monitor).

Key takeaways

  • SSH-and-grep is reactive, unscalable and historyless — measure continuously and alert automatically.
  • Instrument with counters, gauges and histograms plus labels; watch the golden signals.
  • Prefer pull (scrape /metrics) for control + free liveness; add a push gateway for ephemeral jobs.
  • Store the sample firehose in a time-series database — the compressed, range-queried heart.
  • Split alerting: a rule evaluator fires (FOR a duration); an alert manager dedupes, groups, routes.
  • Dashboards + a query language turn detection into diagnosis across labels.
  • Scale with HA + sharded collectors and federation; cap cardinality; and monitor the monitor.

Concepts covered

  • What is a monitoring system?
  • SSH-and-grep doesn't scale
  • Instrumentation & metric types
  • Pull vs push collection
  • Store it in a TSDB
  • Alerting: rules, dedupe & routing
  • Dashboards & queries
  • Scale: HA collectors & federation
  • Alert storms & monitoring the monitor
built to tell you the system is dying before your users do — make the calls, kill a scraper, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs