InterviewLegend

Design the Metrics System

Frequently asked
Alex Chen45 min readUpdated 74 real interview reports
In this lesson
  • Scope a metrics pipeline that ingests a firehose of points and serves tag-filtered, time-range queries fast
  • Walk the agent-to-query path end to end, naming the buffer, the store, and the tag index that make it work
  • Reason through the parts that separate a senior answer: rollups, percentile sketches, the tag index, and the write firehose

Understanding the problem

"Design a monitoring service like Datadog or SignalFx, assume an agent on every host" is the Datadog system design round in one sentence, and it is the most-asked design prompt in the loop. The shape is concrete. Millions of hosts each run a small agent that emits metrics every ten to fifteen seconds: a name like request.latency, a set of tags like service:web, region:us-east, host:i-0abc, a numeric value, and a timestamp. Our job is to ingest that firehose, store it cheaply, then answer queries like "p95 latency for service=web over the last hour, broken down by region" in under a second. Everything downstream is shaped by that volume.

Get the vocabulary down before anything else; every later choice hangs on it. A single metric point is the four-tuple of name, tags, value, and timestamp. A time series is the identity formed by a name plus a specific tag set: request.latency{service:web, region:us-east, host:i-0abc} is one series, and the same metric reported from a different host is a different series. Storage is organized around series, not individual points, and a query selects a set of series by their tags before aggregating over a time window. Fix that early. The whole design rotates on the series and its tag set, read over a window.

The Datadog angle
This isn't "something like Datadog." It's Datadog's actual product, and the person across the table built it. A hand-wave that would pass elsewhere stands out fast. They know where the bodies are buried: the cardinality of the tag index, the cost of storing raw points forever, the impossibility of averaging percentiles. This is also where seniors get sunk, the candidates who clear both coding rounds and then never get concrete on design. So reach for the real mechanisms. Agents, a Kafka buffer, rollups, a per-series sketch, the way someone who has read the engineering blog would. The scale number isn't background. It's the prompt.

Functional requirements

Hold the scope to three verbs: ingest, store, query. The agent reports points, we durably accept them, and a user runs a time-range query that filters by tags and aggregates. The one aggregation we must get right is the percentile, because the round leans on it and because it is the part that breaks naive designs. Everything else is below the line.

In scope

  • Ingest metric points from an agent on every host (name, tags, value, timestamp)
  • Store time series cheaply at six-months retention
  • Query a tag-filtered set of series over a time range (service=web AND region=us-east)
  • Aggregate over the window, including percentiles (p50, p95, p99) and group-by tags

Below the line (call it out, then skip)

  • Alerting and the monitor evaluation loop (you build that engine as a deep dive in the budgeting-app design, next). A textbook "design Datadog" sometimes folds alerting in, so name it as a deliberate scope cut rather than an omission
  • Logs and traces (separate pipelines with their own storage)
  • The dashboard rendering and the UI (assume the web client exists)
  • The agent's internal collection logic (we treat it as a client that POSTs)
  • Multi-tenant billing, access control, and quotas

Non-functional requirements

Put a number on each one, because these decide the architecture. Write volume is the dominant constraint. We accept on the order of a billion points per second at peak, and we must never drop the firehose when storage hiccups, so a buffer has to decouple ingest from the write path. Reads are far rarer than writes but latency-sensitive: a dashboard query should return in well under a second even when it touches millions of series. Freshness can be relaxed to a few seconds. A point reported now showing up on a graph a handful of seconds later is fine, which buys us an asynchronous pipeline. And percentiles are allowed to be approximate, accurate to a percent or two, which is the single relaxation that makes the design affordable.

Cost is a first-class requirement here, because storing every raw point at this volume is the fastest way to bankrupt the design. We will trade exactness for money in two places, downsampling old data and approximating percentiles, and we flag both now so the deep dives can cash them in. Availability matters more than strong consistency: a metrics backend that is briefly behind is acceptable, but the worst possible moment to be down is the middle of an outage. So we accept being eventually-correct rather than blocking writes to stay consistent. Stay up first. Be exactly right second.

  • ~100T Points per day. ~1.2B points/sec at peak.
  • < 1 s Query latency. Even across millions of series.
  • ~Few sec Freshness. Async pipeline is fine.
  • Approx OK Percentiles. Accurate to a percent or two.

The set-up

Core entities

Name the data the system passes around first, then draw. Four carry the design:

1

MetricPoint

One emitted reading: a metric name, a tag set (an unordered bag of key:value pairs), a numeric value, and a timestamp. This is the firehose unit, billions a second. We never want to store it one-row-each forever.

2

TimeSeries

The identity formed by a metric name plus one exact tag set. All points sharing that identity form a single series. Storage and rollups are keyed by a series_id, a stable hash of name plus the sorted tag set, so the same series always lands in the same place.

3

Rollup

A pre-aggregated summary of one series over one time window: a per-minute or per-hour bucket holding sum, count, min, max, and a percentile sketch. Rollups are what we keep at scale; raw points are short-lived.

4

TagIndex entry

The inverted-index mapping from a single tag (region:us-east) to the set of series_ids carrying it. Queries intersect these posting lists to find which series a filter selects, without scanning every series.

The store under the rollups is a time-series store. We will name Cassandra as the concrete pick. The access pattern is a perfect fit, and it is the database the original Datadog prompt explicitly asks you to reach for. Every write is an append keyed by series and time. Every read is a contiguous range scan over one series within a window. There is essentially no update-in-place. We partition by series_id so one series lives together and a range scan is sequential, and we order within a partition by timestamp so a time-range read is one continuous sweep. Cassandra's wide-row, write-optimized, horizontally-sharded model is built for exactly this, and it sustains the write rate by spreading partitions across many nodes. The tag index is a separate store, an inverted index we discuss in its own deep dive, because mixing the "which series" lookup into the time-ordered store would wreck both access patterns.

A real metrics store, named so you can defend it. Cassandra is the recognizable, defensible answer and it maps cleanly onto wide-row time-series storage. If the interviewer pushes, the purpose-built option is a dedicated TSDB, and Datadog's own backend is a custom columnar store called Husky built on object storage. Lead with Cassandra, name Husky as the production reality, and you have shown both the category instinct and the company knowledge.

API

Two endpoints carry everything. The agent batches many points into one POST every interval rather than one request per point, because a billion tiny HTTP calls a second would melt any gateway. The query endpoint takes a metric and a tag filter over a time range, plus the aggregation to apply, and returns a series of timestamped values, already rolled up to a resolution that suits the range.

The interface
RequestFire-and-forget
the agent ships a batch every ~10-15s
POST /api/v1/series
202 Accepted
buffered, not yet durable in the store
query a tag-filtered metric over a time range
GET /api/v1/query
200 { series: [ { tags: {region:"us-east"}, points: [[ts, val], ...] } ] }
aggregation like p95, grouped by a tag

The POST returns 202 Accepted on purpose, where a 200 OK would overpromise. It tells the agent we durably buffered the batch, and says nothing about whether the batch is queryable yet. That single status code is the contract that lets us put a buffer between ingest and storage, which is what survives the firehose. The query parameters preview the three deep dives that matter: agg=p95 forces the percentile question, filter forces the tag index, and from/to forces the rollup-and-retention story. We build each below.

High-level design

Go requirement by requirement, standing up the simplest working version before any hardening. The colors below sort components by role. Blue is the Agents on every host. Violet is the services: the Ingest gateway, the Write consumers, and the Query service. Amber is the Kafka buffer that absorbs the firehose. Green is the Time-series store and the Tag index. The walkthrough tags each component the same way.

Loading diagram...

1) Ingest the firehose. The Agents batch points and POST them every interval to the Ingest gateway, a stateless fleet that authenticates and lightly validates and does nothing slow. It writes each point straight to the Kafka buffer keyed by series id and returns 202. The gateway never touches a database, so it scales out flatly and stays cheap. And because the agent batches, the gateway sees a manageable rate of fat requests rather than a billion tiny ones. The naive version we are deliberately not building yet would have the gateway write straight to the store synchronously, which collapses the instant the store hiccups. Deep dive four explains exactly why the buffer is not optional.

2) Buffer the write firehose in Kafka. A queue earns its box here because the firehose is bursty and the store cannot be allowed to apply backpressure all the way to the agents: bursts have to wait somewhere that is not the request path. We partition the Kafka buffer by series id so every point of one series lands on the same partition in timestamp order. That ordering lets a single consumer aggregate a window with no cross-partition coordination. Kafka holds points durably for a retention window, so a slow or restarting store loses nothing. What we punt on here is duplicates: at-least-once delivery means a point can be replayed, and a percentile or a count that double-counts is wrong. Deep dive four settles the dedupe.

3) Roll up and store. The Write consumers read each partition and do the aggregation the design is built around. Rather than store every raw point, a consumer groups a series' points into a per-minute window and writes one Time-series store row holding the window's sum, count, min, max, and a percentile sketch. The first time it sees a series it also registers that series in the Tag index. The store is partitioned by series id and ordered by time, so this is an append. We are punting two big questions. Deep dive one is how rollups and retention keep 100T points a day from exploding the bill. Deep dive two is how a sketch lets us serve p95 without keeping raw values.

4) Serve the query. A dashboard hits the Query service with a metric and a tag filter over a time range, plus the aggregation to apply. It first asks the Tag index which series ids the filter selects, intersecting the posting lists for service:web and region:us-east. Then it range-scans those series' rollups from the Time-series store at a resolution that fits the range. An hour query reads per-minute rollups, a month query reads per-hour ones. It merges the per-window aggregates across series and returns the result. It reads rollups, never raw points. Reads are rare. Writes are the firehose. The punt is the index itself: resolving a filter over millions of series fast, and surviving high-cardinality tags, is deep dive three.

Technology choices

  • Ingest gateway is a stateless service fleet (any language, Datadog’s real agent and intake are Go) behind a load balancer. It holds no state, so when one falls over the load balancer routes around it and a new one spins up cold. The only failure that matters is the gateway outrunning Kafka, which it can’t for long because writing to a partition is a fast append; if a Kafka broker is unavailable the gateway writes to another in-sync replica.
  • Kafka buffer: Kafka is the event log that decouples ingest from storage. Partitioned by series id so one series is one ordered stream; replicated across brokers so a broker failure promotes an in-sync replica and the consumer resumes from its committed offset with no lost points. Consumer lag, how far behind the latest offset the consumers are, is the freshness dial: rising lag means the store is struggling, and we can scale consumers or shed load before dropping anything.
  • Write consumers are a consumer group, one set of partitions per consumer, that does the rollup-and-sketch work. If a consumer dies, its partitions are reassigned and the replacement replays from the last committed offset, which is why the rollup write has to be idempotent (keyed by series plus window) so a replay overwrites rather than double-counts. This is the work-queue-versus-event-log distinction in action: Kafka is an event log we can rewind and replay, not a queue that deletes on read.
  • Time-series store: Cassandra (or Datadog’s columnar Husky on object storage). Wide-row and write-optimized: partition key is the series id, clustering key is the timestamp, so a write is an append and a time-range read is one sequential scan. Horizontally sharded across nodes to sustain the write rate; replication factor three for durability and to survive a node loss. The probe it invites, "what if a hot series overwhelms one partition," is answered by sharding a hot series across sub-partitions by time bucket.
  • Tag index is an inverted index from tag to series ids, the same idea as a search index, served from a store tuned for set intersection (Elasticsearch-style postings, or a custom index). Its failure mode is not a crash but cardinality: a tag with millions of distinct values bloats the postings and slows intersection, which deep dive three handles head-on. The brand is less important than the category; what matters is that it answers "which series match this filter" without a full scan.

Potential deep dives

At Datadog the deep dives decide the level, and most of the signal sits here. The interviewer locks onto one part of the pipeline and pushes until the cost or the correctness gives way. The four below recur the most. Each is scored against the same tiers, and each opens on the number that forces the design.

1) How do we store 100T points a day without the cost exploding?

Start with the arithmetic, because it is the whole reason this design is hard. Take two million hosts, each emitting on the order of 8,640 distinct series, reporting every fifteen seconds. A day has 86,400 seconds, so a series reports 86,400 / 15 = 5,760 times a day. Multiply: 2,000,000 hosts × 8,640 series × 5,760 points ≈ 1.0 × 10^14. That is about 100 trillion points a day. Average that over the day and it is 100T / 86,400s ≈ 1.2 billion points a second. If each raw point costs roughly 12 bytes (a timestamp and a value), 100T points is about 1.2 petabytes a day uncompressed, and storing six months of that as raw rows is hundreds of petabytes. Nobody pays that bill. The data almost nobody queries at one-second resolution months later is not worth keeping that way.

BadStore every raw point in its own row, keep it forever

Approach

Write each incoming point as a (series_id, timestamp, value) row in the store and never delete it. Queries scan the raw rows for the requested window and aggregate on the fly.

Challenges

The cost grows without bound, about 1.2 PB a day, and a six-month query has to scan months of one-second-resolution rows to draw a single line on a graph. Nobody looks at last quarter at per-second detail, so we are paying to store and scan resolution no one wants. Worse, every query redoes aggregation the write side could have done once. Name it, then knock it down and move on.

What forces the upgrade is the observation that resolution and retention are inversely valuable: recent data is read at fine resolution, old data is read coarsely or not at all. So we do the aggregation once, on the write path, and we age data down through resolution tiers instead of keeping it raw.

GreatRoll up at write time, then downsample through retention tiers

Approach

The Write consumers aggregate each series into fixed windows as they ingest. A per-minute rollup collapses 60/15 = 4 raw points into one row holding sum, count, min, max, and a sketch, a 4× reduction immediately. Then retention tiers age the data down: keep raw points for only a few hours (long enough to debug a live incident), keep per-minute rollups for, say, fifteen days, and keep per-hour rollups for the full six to fifteen months. A per-hour rollup is 60 per-minute rows collapsed into one, another 60×. Old high-resolution data simply expires; a background job rolls per-minute into per-hour and drops the finer tier. Queries read the coarsest tier that still answers the question: a one-hour query reads per-minute rollups, a one-month query reads per-hour rollups, so a long-range graph scans thousands of rows, not billions.

Challenges

Rollups are lossy: once a per-minute bucket is written you cannot reconstruct the four raw values inside it, so the retention windows have to match how people actually debug (a few hours of raw is the number incident responders need). Sum, count, min, and max all roll up cleanly by adding or taking extremes across windows, but a percentile does not, which is exactly why the sketch rides along in every rollup and why deep dive two exists. And a rollup write must be idempotent, keyed by series plus window, so a Kafka replay overwrites the bucket rather than double-adding into it.

The scaling writes lesson covers absorbing and pre-aggregating a high write volume, and Cassandra and DynamoDB covers the wide-row, time-partitioned store the rollups live in.

The Datadog angle
Rollups and retention tiers are how the product gets priced, not just built. Datadog keeps metrics at full resolution for a short window and downsamples older data into coarser rollups. Retention is a literal line item on the bill. A candidate who lands on tiered retention is describing the real cost model, not a textbook one. Put it bluntly: you don't store 100T points a day, you store rollups of them, and you keep raw data only as long as someone might debug with it. That's metrics economics the way the team lives it, where every byte kept for a year gets multiplied by trillions of series.

2) How do we serve p95 without storing every raw value?

This is the percentile-over-time-series question, asked almost word for word, and it is the one naive designs get wrong twice. A p95 for one series over one minute means: of all the latency values in that minute, the value below which 95% of them fall. Two facts make it hard. First, at this volume a single busy series can record millions of values a minute, so keeping every value to sort later is exactly the cost we just refused to pay. Second, and less obvious, you cannot average percentiles. If host A's p95 is 100ms and host B's p95 is 200ms, the combined fleet's p95 is not 150ms. A percentile is a position in a sorted distribution, and positions do not add. So we need a structure that summarizes a distribution cheaply and, crucially, can be merged across hosts and windows.

BadStore every raw value, or average the per-host p95s

Approach

Either keep all the raw latency values so any percentile can be computed exactly at query time, or have each host compute its own p95 and average those together across the fleet.

Challenges

Both are wrong, in opposite ways. Storing every raw value is the 100T-points-a-day cost we already rejected, just relocated, and it makes the rollup pointless. Averaging the per-host percentiles is statistically invalid: a percentile is a rank in a distribution, not a quantity you can add, so the average of two p95s is a number that means nothing. Say "average the p95s" in the room and the interviewer knows you haven't thought about what a percentile is. Be ready to say exactly why it breaks.

What we need is a small, fixed-size summary of a distribution that gives an approximate percentile and, the key property, can be merged: combine two hosts' summaries and read the fleet's percentile off the result. That structure is a histogram-style sketch, and it ties straight back to the coding round.

GreatA mergeable per-window percentile sketch

Approach

Each Write consumers keeps, per series per window, a sketch: a compact set of buckets, each holding a count of values that fell in its range, where the bucket boundaries are log-scaled so resolution stays proportional from microseconds to seconds. To read a percentile you scan buckets low to high, summing counts, until the running total crosses the rank you want, then report that bucket’s bound. The answer is approximate, accurate to a bucket’s width, which is the percent-or-two error the requirements allowed. The magic property is mergeability: because two sketches over the same bucket layout combine by adding their counts bucket-by-bucket, the Query service reads each series’ sketch from a window, adds them, and reads p95 off the merged result. The same merge rolls a minute of sketches into an hour. Datadog open-sourced exactly this structure, DDSketch, with a guaranteed relative-error bound; t-digest is the other well-known mergeable sketch.

Challenges

The error is bounded but real, so you state it: a percentile read from a sketch is accurate to the bucket width, which log-scaling keeps to a fixed relative percentage rather than letting the slow tail collapse into one bucket. Merging requires a shared bucket layout across all hosts, so the sketch parameters are fixed fleet-wide, not chosen per host. And the sketch costs more bytes than a single sum or count, a few hundred bytes per window per series, which is the price of being able to answer percentiles at all; it is still vastly cheaper than the raw values.

Loading diagram...

This is the same structure the latency histograms coding round builds by hand: bucket counts, log-scale buckets for the tail, a percentile read as a running-sum scan until you cross a rank, and merging two histograms by adding their counts. The coding round implements one host's sketch; this design distributes it across the fleet. Saying "this is the histogram from the coding round, made mergeable and rolled up over time" connects the two rounds in a way that lands.

The Datadog angle
The percentile sketch is literally Datadog's, and it bridges straight to the coding loop. DDSketch is a Datadog invention with a published relative-error guarantee, built precisely because you can't average percentiles across a fleet and can't afford to store every value. The product ingests latency as distributions, merges them across hosts and time, and serves p50, p95, and p99 off the merged sketch. Name a mergeable sketch, explain why averaging p95s is invalid, tie it back to a Premium question coding problem, and you've handled the hardest idea in the metrics backend. Of the four dives, this is the one that pushes a senior answer toward staff.

3) How do we serve a tag-filtered query fast over millions of series?

A query like service:web AND region:us-east has to find which series match before it can read any data, and the count is the problem. With two million hosts each carrying thousands of series, there are on the order of hundreds of millions to billions of active series. Checking each one's tags against the filter is a full scan of billions of records for every dashboard refresh, which blows the sub-second budget instantly. We need to go from filter to matching series ids without looking at the ones that do not match. A full scan is off the table.

BadScan every series and test its tags

Approach

Keep series with their tag sets and, for each query, iterate over all series, keeping the ones whose tags satisfy the filter.

Challenges

It is a scan over hundreds of millions of series per query, which cannot meet a sub-second latency target no matter how the store is tuned, and it gets slower as the fleet grows. Every dashboard tile would pay this cost on every refresh. The fix is the standard one for "find the items matching a set of attributes," which is an inverted index, the same structure a search engine uses to go from a word to the documents containing it.

GreatAn inverted index on tags, with posting-list intersection

Approach

Build the Tag index as an inverted index: for each tag value, keep a sorted posting list of the series ids carrying it. service:web maps to one list, region:us-east to another. A filter is answered by intersecting the posting lists, an AND is a set intersection and an OR is a union, which over sorted lists is a fast merge that touches only the matching series and skips the rest. The result is the exact set of series ids to range-scan in the time-series store, so the expensive data read happens only for series that actually match. The consumers register a series in the index the first time they see it, which is rare relative to writes because most points belong to series that already exist, so the index is written far less often than the firehose flows.

Challenges

The trap is cardinality, and it is the real Datadog pain. The index size and intersection cost scale with the number of distinct tag values, so a low-cardinality tag like region (a handful of values) is cheap, but a high-cardinality tag like user_id or request_id (millions of distinct values) explodes the index: millions of one-entry posting lists, a series count that balloons because every distinct tag combination is a new series, and intersections that crawl. This is why Datadog meters custom metrics by cardinality and why putting an unbounded id in a tag is the classic way to blow up a metrics bill. The mitigations are real: cap or reject runaway-cardinality tags at ingest, separate genuinely high-cardinality dimensions out of the metrics path, and treat cardinality as a budgeted resource rather than a free dimension.

Elasticsearch covers inverted indexes and posting-list intersection, the same machinery a search engine uses to map a term to its documents.

The Datadog angle
Cardinality is the most famous failure mode in the product. "High-cardinality tags blow up the index" is not a textbook footnote here. It is the thing that drives Datadog's custom-metric pricing and the number-one way customers accidentally run up a bill. Two things matter. Do you see that a tag like user_id multiplies the series count and the index size without bound? And would you defend the system by metering or rejecting it at ingest? Bring up cardinality yourself, and you've named the one place this exact system breaks in production.

4) How do we handle the write firehose without dropping points?

Back to the rate: roughly 1.2 billion points a second on average, and far higher in bursts when a fleet redeploys or an incident makes everything noisy at once. The store cannot absorb that directly, and the one thing a metrics backend must never do is silently drop points during the exact incident an engineer is staring at the dashboard for. So the question is how ingest stays up and lossless when the store is slow or briefly down. Losing points is not allowed.

BadWrite straight from the gateway to the store, synchronously

Approach

The ingest gateway takes each batch and writes it directly into the time-series store, returning success once the store acknowledges.

Challenges

The store’s availability becomes the ingest path’s availability. A node restart, a compaction pause, or a hot partition makes the store slow, and that backpressure propagates all the way to the agents, which then either block or drop points on the floor, precisely when load is highest. There is no place for a burst to wait, so a burst becomes data loss. And the bursts are not small: a synchronized fleet redeploy or a noisy incident can spike the rate several times over the 1.2-billion-a-second average for minutes at a stretch, which is exactly when the data matters most and exactly when a synchronous store is most likely to stall. This is the design the buffer exists to prevent.

GreatKafka as the shock absorber, with idempotent consumers

Approach

Put the Kafka buffer between the Ingest gateway and the store, partitioned by series id. The gateway’s job ends at a durable append to Kafka, which is fast and decoupled from the store’s health, so it returns 202 immediately. The Write consumers drain Kafka at their own pace. When the store slows, points pile up safely in Kafka instead of being dropped. Consumer lag rises, which is the freshness dial we alert on, and we scale consumers or shed low-priority metrics rather than lose data. Partitioning by series id keeps each series’ points ordered on one partition, so a consumer aggregates a window without coordinating across partitions. Delivery is at-least-once, so a rebalance or restart can replay points; we make the rollup write idempotent by keying each bucket on series id plus window, so a replayed point lands in the same bucket and a re-applied rollup overwrites rather than double-counting.

Challenges

Kafka’s own durability has to be real: replication factor three and acknowledged writes, or the buffer that was supposed to prevent loss becomes the place loss happens. Idempotency is non-negotiable because at-least-once will replay, and a double-counted point corrupts a sum or inflates a sketch, so the series-plus-window key has to be the dedupe boundary. And the buffer trades latency for safety: a point is durable in milliseconds but queryable only after a consumer rolls its window, which is the few-seconds freshness the requirements already accepted. Under sustained overload the honest move is graceful degradation, shedding the least important metrics, never dropping silently.

One follow-up the interviewer reliably reaches for: partitioning by series id is right for ordering, but it can create a hot partition. A handful of extremely busy series, say a metric on a load-balancer tier that every request touches, can pile far more volume onto their partitions than onto others. One overloaded partition then slows the consumers that own it. The fix is to spread a hot series across sub-partitions, hashing on series id plus a small time bucket so its load fans out, then merging the sub-partition rollups back together at write time. The same hot-key reasoning shows up on the read side, where one viral series is read far more than the rest. A small read cache in front of the Query service absorbs that.

The scaling writes lesson covers buffering a high write rate, and Kafka covers partitioning and consumer groups, plus the offset replay and at-least-once delivery that force idempotent consumers.

The Datadog angle
A Kafka buffer in front of the metrics store is the architecture the prompt itself names, and the one Datadog runs. The prompt often spells out the toolbox: "describe the libraries or frameworks, Kafka, Cassandra." So Kafka as the firehose buffer and Cassandra-style storage answers the question on its own terms. Datadog runs one of the largest Kafka deployments anywhere, precisely to decouple ingest from storage at this scale. And the buffer isn't there for throughput alone. It's there so a store hiccup during an incident never costs you the metrics you need most at that exact moment.

What is expected at each level

That is a lot of surface for one pipeline. How far you take it scales with the level you are targeting, and the round leans on depth, because the reject that recurs is a strong coder who never gets concrete about design.

Mid-level

Breadth first. Get the pipeline right end to end. Agents batch and POST, a gateway writes to a buffer, consumers roll points up into per-minute aggregates and store them, and a query reads those rollups over a time range. The one cost idea to land is that you store rollups, not every raw point, and you should be able to say why a per-minute aggregate is cheaper than the raw rows it replaces. The interviewer will drive the percentile and tag-index depth. The bar: a clean, working pipeline with rollups, where you can explain why keeping raw points forever is too expensive. It is fine if the sketch and the cardinality cases need prompting.

Senior

Breadth first, then real depth you reach on your own. Two problems should surface without prompting. First, percentiles: you cannot store every value, and you cannot average per-host p95s, so you keep a small mergeable sketch per window instead. Second, the tag filter: an inverted index from tag to series ids, intersected as posting lists, is what turns a filter into a set of series without scanning them all. Defend Kafka as a deliberate buffer too, not decoration. Consumer lag is the freshness dial you watch, and idempotency is how you survive a replay. The bar: you find the percentile and tag-filter bottlenecks without being led to them, take each to a Great tier, and explain why you picked the buffer and the time-series store.

Staff+

By the staff bar the scope widens, and the cardinality fight is yours to drive. Treat cardinality as a budgeted resource: you cap or reject runaway tags right at ingest. Own the retention tiers too. Raw data ages into per-minute rollups and then per-hour ones, and that is how you keep the cost in line. Drive the mergeability of the sketch as the one property that makes a fleet-wide percentile possible at all. Then widen out. Add multi-region ingest, where each region runs its own buffer and store, and treat cost as a lever you pull at every layer. Under overload, you shed low-priority metrics on purpose rather than letting points drop silently, and you call that a design decision, not an accident. The bar: you lead and the interviewer only nudges, never rescues, and you reason about cost and cardinality end to end. The percentile sketch and the retention tiers stay at the center of your design, which is where a strong answer keeps them.

Key Takeaways

  • ✓The volume is the design. At ~100T points a day and ~1.2B/sec, you do not store raw points, you roll them up at write time and age them down through retention tiers (raw for hours, per-minute for days, per-hour for months).
  • ✓You cannot average percentiles and cannot afford to store every value, so each series carries a mergeable sketch (DDSketch / t-digest) per window. Percentiles are approximate, accurate to a bucket width, and combine across hosts by adding bucket counts, the same histogram as the latency-histograms coding round.
  • ✓A tag-filtered query needs an inverted index from tag to series ids, intersected as posting lists. Its failure mode is cardinality: a high-cardinality tag like user_id explodes the index and the series count, which is the real Datadog cost trap.
  • ✓Kafka is the shock absorber that keeps the firehose lossless when the store hiccups: partitioned by series id, with idempotent consumers (keyed by series plus window) to survive at-least-once replays, and consumer lag as the freshness dial.
Had your Datadog interview?Tell us what they asked. Reviewed before it goes in, never with your name.
Datadog interview guide

Know what Datadog asks before you walk in.

11 coding · 4 system design · 30 behavioral questions

Get Premium$49 a year · Cancel anytime
Or start freeNo card.

Get their questions first.

InterviewLegend is independent and not affiliated with or endorsed by any company it covers.