System design interview guide
Time-Series Database System Design Interview: Designing a TSDB for Massive Append-Heavy Writes, High-Cardinality Metrics, and Fast Range Aggregations at Scale
A time-series database is built around one dominant fact: writes arrive constantly, in enormous volume, almost always with a timestamp near now, and they are almost never updated after they land. A single fleet of servers or IoT devices can push millions of points per second, and each point is tiny, often just a timestamp and a float. The interesting engineering is not storing that data once, it is storing months of it cheaply, keeping the most recent slice hot for dashboards, and answering range and aggregation queries (p99 latency per service over the last 6 hours, grouped into 1-minute buckets) in well under a second. The whole design bends around append-heavy ingestion, aggressive compression, and time-based lifecycle management. Any throughput, cardinality, or compression figure here is an industry-typical range or an illustration, not a measured vendor-internal number.
A time-series database (TSDB) is a store optimized for data that is a sequence of timestamped values, such as CPU usage, request latency, sensor readings, or stock prices. Instead of thinking in rows and joins, it thinks in series: a metric name plus a set of key-value tags (labels) uniquely identifies one series, and each series is an ordered stream of (timestamp, value) points. Writes are append-heavy and mostly in timestamp order, reads are heavily skewed toward recent data, and queries are dominated by range scans and time-bucketed aggregations rather than point lookups. This workload breaks the assumptions of a general-purpose relational row store, so a TSDB makes different choices: it stores points columnar and per-series so that a scan touches only the columns and time ranges it needs, it compresses hard using delta-of-delta encoding on timestamps and XOR (Gorilla-style) encoding on floats to get roughly an order of magnitude reduction, and it partitions data by time so old data can be dropped or downsampled in bulk. Ingestion typically flows through a write-ahead log for durability and an in-memory head block for recent points, which is periodically flushed to immutable on-disk blocks. The central failure mode is cardinality: because every unique tag combination is a new series, a single high-cardinality label (a user id, a request id, a container id that churns) can explode the series count and blow up memory and index size. Retention and downsampling (raw to 1-minute to 1-hour rollups) keep long-term storage affordable, and the query engine adds time-series-native functions like rate, percentiles, and group-by-time. TSDBs lean toward cheap high-volume ingestion and fast recent-range queries, and away from arbitrary updates, cross-series joins, and unbounded cardinality.
Where it shows up
Asked at observability and infrastructure companies most directly (Datadog, Grafana Labs, Chronosphere, InfluxData), and at any company that runs large monitoring or metrics platforms internally: Meta (which built Gorilla and its in-memory TSDB), Google (Monarch, and the Borgmon lineage that inspired Prometheus), Uber (M3), Netflix (Atlas), and companies in IoT, fintech market data, and industrial telemetry. It shows up both as a standalone infrastructure question (design a metrics/monitoring backend or design a TSDB) and as a component inside larger designs such as an alerting system, an APM product, a rate limiter with historical counters, or an IoT ingestion pipeline.
Why this question is asked
Interviewers like this problem because you cannot bluff it by naming a product. It forces you to reason from the shape of the workload: writes vastly outnumber and outpace reads, the data is immutable once written, and access is overwhelmingly recent. From those facts, a strong candidate independently derives why a row store struggles, why columnar per-series layout wins, why compression is not a nice-to-have but a first-class design goal, and why time partitioning makes retention almost free. It also surfaces one genuinely hard, non-obvious failure mode, high cardinality, that separates people who have operated these systems from people who have only read about them. And it rewards honesty about limits: a TSDB is not a general database, it usually cannot update history efficiently, it does not join across series cheaply, and it will fall over if you let cardinality grow without bound. Finally it invites concrete trade-off debates (push versus pull ingestion, LSM versus a specialized append-only engine, raw retention versus downsampling) where there is no single right answer, which is exactly what interviewers want to probe.
Requirements
Always clarify these in the first 5 minutes of the interview. Do not start drawing boxes until both lists are agreed.
Functional requirements
- Ingest timestamped points, each identified by a metric name plus a set of key-value tags, at high volume and mostly in time order
- Retrieve the points of a series over a time range, and scan many series that match a tag filter
- Run time-bucketed aggregations (group by time) and time-series functions: rate and increase over counters, sum, avg, min, max, and percentiles across series
- Filter and group by tags, so a single query can slice a metric along any labeled dimension
- Downsample raw data into lower-resolution rollups (raw to 1-minute to 1-hour) and query the coarsest resolution that answers the question
- Enforce per-tier retention so raw and rolled-up data expire on their own schedules
- Expose recent data with low latency for dashboards while keeping long-range history affordable
Non-functional requirements
- Sustain very high append-heavy write throughput (millions of points per second in large deployments)
- Answer typical recent-range dashboard queries in well under a second
- Store history cheaply through aggressive, time-aware compression (roughly an order of magnitude)
- Scale primarily on active series (cardinality), sharding across nodes when one machine's memory is exceeded
- Durability of acknowledged writes via a write-ahead log, with replication across failure domains in a distributed setup
- Bounded, predictable memory and index growth, with defenses against cardinality explosions
- Cheap lifecycle operations: retention and expiry by dropping whole time-bounded blocks rather than row-by-row deletes
Back-of-envelope scale estimates
Show your math. Pulling numbers from thin air signals you have not thought about the load.
Ingestion rate
1M to 10M+ points/sec (large deployment)
A single mid-size fleet emitting a few hundred metrics per host every 10 to 15 seconds across tens of thousands of hosts already lands in the low millions of points per second. Large observability backends ingest far more. This is the primary sizing dimension and drives the write path, WAL throughput, and compression design. Treat as an illustrative range.
Active series (cardinality)
1M to 100M+ active series
Active series count, not raw point rate, is usually the real scaling limit, because each series needs an index entry and in-memory head state. A few million series is comfortable for a single node; tens of millions typically requires horizontal sharding. Derived from typical per-series memory overhead multiplied by series count.
Point on-disk size after compression
~1 to 2 bytes per point (typical)
A raw point (16-byte timestamp plus 8-byte float, plus overhead) compresses dramatically once you apply delta-of-delta on timestamps and XOR on values. The Gorilla paper reported roughly 1.37 bytes per point on Facebook production data. This is what makes months of retention affordable. Workload-dependent, so treat as typical.
Compression ratio
~10x to 12x versus naive encoding
Gorilla-style encoding took 16 bytes per point down to around 1.37 bytes, roughly a 12x reduction, because regularly sampled timestamps and slowly changing values encode into very few bits. Actual ratio depends on how regular the timestamps are and how much the values move.
Retention tiers
raw ~days to weeks, rollups ~months to years
Raw high-resolution data is expensive to keep, so it is retained for a short window (for example 15 days), while downsampled 1-minute and 1-hour rollups are kept far longer (months or years) at a fraction of the size. Illustrative policy, tuned per deployment.
Recent-read latency target
sub-second for typical dashboard range queries
Dashboards refresh every few seconds and query recent windows, so the hot path (in-memory head plus most recent on-disk blocks) must answer range and aggregation queries in well under a second. This is a design target, not a per-query guarantee.
High-level architecture
A client (an agent on a host, an application SDK, or a scrape target) produces points, each carrying a metric name, a set of tags, a timestamp, and a value. Those points enter through an ingestion tier whose job is to parse and validate them, resolve each point to its series (the unique combination of metric name plus tags), and route it to the right storage shard. Ingestion can be push based, where clients send data to the database, or pull based, where the database scrapes an HTTP endpoint on each target on a schedule. Either way the ingestion tier is where per-series identity is established and where the first cardinality defenses (limits, relabeling, dropping high-churn labels) are applied. In a sharded deployment, series are distributed across storage nodes by hashing the series identity, so that each node owns a subset of series end to end. On the write path inside a storage node, a newly arrived point is first appended to a write-ahead log for durability and then inserted into an in-memory structure, usually called the head block, that holds the most recent window of data for every active series. The head block is what serves the freshest reads and is also where incoming points are buffered before compression. Periodically the head is cut and flushed to disk as an immutable, compressed block covering a fixed time range (for example two hours). Once a block is safely on disk, the corresponding WAL segments can be discarded. This head-plus-immutable-blocks pattern is the heart of most modern TSDBs: recent data lives in fast mutable memory, historical data lives in tightly packed, read-only, time-bounded files. Storage is organized so that a query touches as little data as possible. Within a block, points are stored columnar and grouped per series, with timestamps and values in separate, heavily compressed streams (delta-of-delta for time, XOR for floats). Alongside the sample data sits an inverted index that maps each tag key-value pair (for example service=checkout or region=us-east) to the set of series that carry it, so a query with a tag matcher can quickly find exactly the series it needs without scanning everything. Blocks are named and laid out by time range, which makes both querying a time window and enforcing retention (delete whole blocks older than the cutoff) cheap. Behind the hot path run several background processes that define the long-term economics of the system. A compaction process merges small blocks into larger ones and can deduplicate and re-encode for better compression. A downsampling or rollup process reads raw blocks and precomputes lower-resolution aggregates (raw to 1-minute to 1-hour) so that long-range queries do not have to scan billions of raw points. A retention process enforces TTLs by dropping expired blocks per tier. And in a distributed setup, a replication layer keeps multiple copies of each series across failure domains, while a query layer fans a single query out to all shards that own matching series, merges their partial results, and applies the final aggregation. The design philosophy throughout is that ingestion and recent reads must be fast and local, while the expensive, global work (compaction, downsampling, retention, rebalancing) is pushed into background jobs off the critical path.
In a real interview, sketch this on the whiteboard before diving into any single box.
Core components
Walk through each service. The interviewer wants to hear what each one owns, not just the names.
Ingestion tier (push receiver / pull scraper)
The front door for data. It parses incoming points, validates and normalizes them, resolves each to its series identity (metric plus tags), enforces cardinality and rate limits, and routes to the owning shard. In a push model it exposes a write endpoint that clients send to; in a pull model it scrapes each target metrics endpoint on a fixed interval and attaches scrape-time metadata. This tier is stateless and scales out horizontally.
Write-ahead log (WAL)
A durable, append-only log that every incoming point is written to before it is acknowledged, so that data buffered in memory survives a crash. On restart the node replays the WAL to reconstruct the in-memory head. WAL segments are cheap sequential writes and are reclaimed once the data they cover has been flushed into an immutable on-disk block.
In-memory head block
The mutable, in-RAM structure holding the most recent window of samples for every active series. It absorbs the high write rate, serves the freshest reads directly, and accumulates points until a time boundary triggers a flush. It also tracks which series are currently active, which is the main driver of memory usage and therefore of the cardinality limit.
Immutable on-disk blocks (columnar, per-series)
Time-bounded, read-only files created by flushing the head. Within a block, each series timestamps and values are stored as separate compressed columns, so scans read only what they need and decompression is fast. Because blocks never change after they are written, they are trivially cacheable, safely shareable, and cheap to delete wholesale for retention.
Inverted index (tag / label index)
The structure that makes tag-based queries fast. For every tag key-value pair it stores a posting list of the series ids that carry it, so a query like service=api and region=eu is answered by intersecting posting lists to get the matching series set, without scanning sample data. It is the TSDB analog of a search-engine index and its size grows with cardinality.
Compaction and downsampling engine
A background subsystem that merges many small blocks into fewer large ones (improving compression and read efficiency) and computes rollups, precomputing 1-minute and 1-hour aggregates from raw data. Downsampling is what keeps long-range queries fast and long-term storage affordable, since a year-long query can read hourly rollups instead of billions of raw points.
Retention / TTL manager
The lifecycle enforcer. It applies per-tier retention policies (for example keep raw for 15 days, 1-minute rollups for 90 days, 1-hour rollups for 2 years) by deleting whole expired blocks. Because data is partitioned by time, expiry is a cheap file-level operation rather than a row-by-row delete.
Query engine
The component that plans and executes reads. It uses the inverted index to select series, reads the relevant time ranges from head and blocks, and applies time-series-native operations: range selection, downsampled bucketing (group by time), rate and increase over counters, and aggregations like sum, avg, and percentiles across series. It is optimized to stream and aggregate rather than materialize full result sets.
Sharding and replication layer
In a distributed deployment, the layer that distributes series across nodes (typically by hashing series identity) and keeps replicas across failure domains for durability and availability. On read, a coordinator fans a query out to all shards owning matching series and merges their partial results; on write, it routes each point to the owning shard replicas.
Data model
Pick the right store per table. Justify each choice with the access pattern, not by reflex.
series (identity)series_id (internal)metric_nametags/labels (sorted key-value set)One row per unique combination of metric name and tag set. The series id is an internal handle used everywhere else (index postings, sample blocks). The count of rows here is the cardinality of the system, which is the dominant scaling constraint. Tags are stored sorted and canonicalized so the same label set always maps to the same series.
samples (per-series time/value columns)series_idtimestamp (delta-of-delta encoded)value (XOR / Gorilla encoded)The actual data, stored columnar and grouped by series inside time-bounded blocks. Timestamps and values live in separate compressed streams. Samples are effectively append-only and ordered by time; out-of-order or backfilled points are a special, more expensive case. This is where compression buys the ~10x space reduction.
inverted index (tag postings)label_namelabel_valueposting_list (sorted series_ids)Maps each tag key-value pair to the sorted set of series that carry it. Queries intersect and union these posting lists to resolve tag matchers to a series set before touching samples. Index size and memory grow with the number of distinct label values, so high-cardinality labels are expensive here first.
block metadatablock_idmin_timemax_timeseries_countresolution (raw/1m/1h)replica_locationsCatalog of on-disk blocks. min_time and max_time let the query planner prune blocks that fall outside the requested window, and resolution marks whether a block is raw or a downsampled rollup so the planner can pick the cheapest tier. Retention operates at this granularity by dropping whole blocks.
retention / rollup policytiersource_resolutiontarget_resolutionaggregationsretention_durationDeclares the lifecycle: how raw data is downsampled into coarser tiers, which aggregates are precomputed (min, max, sum, count, last, and percentile sketches), and how long each tier is kept. The downsampling engine and TTL manager both read from this to decide what to compute and what to delete.
Deep dives
These are the conversations the interviewer is steering you toward. Practice each one until you can talk through it without notes.
Why relational row storage struggles with time-series data
A general-purpose relational database stores data row by row, indexes it with a B-tree, and expects a mix of reads, updates, and deletes on individually addressable rows. Time-series data violates almost every assumption behind that design. First, the write rate is enormous and relentless, and B-tree indexes suffer under sustained high-cardinality inserts because every insert may touch and rewrite index pages, causing write amplification and page fragmentation. Second, a row layout interleaves the timestamp, the value, and all the tag columns on disk, so a query that only needs the value column over a time range still drags every other column through memory, wasting I/O and cache. Third, general databases store values in fixed-width, uncompressed form, whereas time-series values are extraordinarily compressible because consecutive timestamps are evenly spaced and consecutive values change slowly; a row store leaves that compression on the table. Fourth, retention in a row store means deleting billions of individual rows, which generates dead tuples, vacuum pressure, and index churn, instead of just dropping a file. You can bolt time-series behavior onto a relational engine (TimescaleDB does exactly this with hypertables that automatically partition a table into time-bounded chunks, plus columnar compression on older chunks), but doing so is precisely an admission that the raw row-store model needs time partitioning, columnar layout, and compression added on top to cope.
The series data model and columnar, per-series storage
The fundamental unit in a TSDB is the series, identified by a metric name plus a set of tags, for example http_requests_total with tags service=checkout, region=us-east, status=500. Every distinct tag combination is a separate series with its own ordered stream of points. This model is powerful because it lets you slice and aggregate along any tag dimension at query time, but it also means the number of series is the product of the cardinalities of all the tags, which is where trouble starts. Storage is organized to match how the data is queried. Points for a given series are kept together and stored columnar: all the timestamps in one compressed stream and all the values in another, rather than interleaved as rows. This matters because a query almost always wants a contiguous time range of one or many series, and often only the values, so reading two tight, sequential, compressed columns is dramatically cheaper than reading scattered rows. Grouping by series also makes compression far more effective, because adjacent timestamps within a series are regularly spaced and adjacent values are correlated, which is exactly the pattern delta and XOR encoders exploit. Columnar per-series layout is the structural decision from which most of the TSDB performance advantages follow.
The ingestion path, WAL, head block, and immutable blocks
The write path is designed so that a point can be durably accepted with minimal work and only later organized for efficient long-term storage. When a point arrives, the node first appends it to a write-ahead log, a cheap sequential write that guarantees the point is not lost if the process crashes before it is flushed. The point is then inserted into the in-memory head block, a mutable structure holding recent samples for every active series, which is what serves the freshest reads. The head accumulates points until a time boundary is reached (Prometheus, for example, cuts blocks on roughly two-hour boundaries), at which point the accumulated data is compressed and written out as an immutable on-disk block covering that time range, and the now-redundant WAL segments are truncated. This separation gives you the best of both worlds: the head absorbs a high, random write rate in memory with a fast durability guarantee from the WAL, while historical data settles into tightly packed, read-only, time-bounded files that are cheap to scan, cache, replicate, and eventually delete. One important wrinkle is out-of-order and backfilled data: because the head and blocks are optimized for near-now, in-order writes, points that arrive far behind the current time are either rejected or handled by a separate, more expensive path, which is why many TSDBs historically restricted how far in the past you could write.
LSM trees versus a specialized append-only TSDB engine
There are two broad ways to build the on-disk engine, and the choice shapes the whole system. One option is to reuse a log-structured merge tree, the same engine family behind Cassandra and RocksDB, which is how Cassandra-based metrics systems and some TSDBs are built. An LSM buffers writes in a memtable, appends to a commit log, flushes sorted immutable SSTables, and compacts them in the background. This is a natural fit for high write throughput and it comes with battle-tested infrastructure, but it is not specialized for time-series: it stores keys and values generically, its compression is general purpose rather than delta-of-delta and XOR, and it can waste space and read effort because it does not know that timestamps are monotonic and values are numeric and slowly changing. The other option is a purpose-built engine, like Prometheus TSDB or InfluxDB TSM, which keeps the same append-only, immutable-block spirit but adds time-series-specific structure: per-series columnar layout, time-bounded blocks aligned for retention, an inverted index for tags, and encoders tuned for regular timestamps and floating-point values. The trade is familiar: reusing an LSM gets you a robust, general engine quickly and leans on existing operational knowledge, while a specialized engine achieves much better compression and query efficiency for this one workload at the cost of building and maintaining bespoke storage code. Most dedicated TSDBs choose the specialized path precisely because the compression and columnar wins are large enough to justify it.
High cardinality as the central failure mode
This is the single most important thing to get right, and the one that most distinguishes real operational experience. Cardinality is the number of distinct active series, and it equals the product of the cardinalities of all the tags attached to a metric. A metric with a service tag (50 values) and a region tag (10 values) has 500 series, which is fine. Add an endpoint tag (200 values) and you have 100,000. Now attach a tag whose value is unbounded or high-churn, a user id, a full URL with query parameters, a request id, a Kubernetes pod name that changes on every deploy, and cardinality explodes into the millions or tens of millions, because every new value creates a brand-new series that must be tracked forever within its retention window. The damage is concentrated in the parts of the system that hold per-series state: the in-memory head keeps structures for every active series, and the inverted index stores a posting list entry for every label value, so both memory and index size grow with cardinality, not with the point rate. A cardinality explosion typically shows up as the ingestion node running out of memory and crashing, or as queries slowing to a crawl because a matcher now has to touch millions of series. This is why a TSDB scales primarily on active series, not on points per second, and why the standard defenses all target cardinality directly: never put unbounded values (ids, raw URLs, timestamps, emails) in tags, aggregate or drop high-cardinality labels at ingest through relabeling, enforce per-metric and per-tenant series limits, and move genuinely high-cardinality dimensions into logs or traces where they belong. A candidate who volunteers the cardinality trap and its mitigations is signaling that they have actually run one of these systems.
Compression, delta-of-delta timestamps and XOR (Gorilla) floats
Compression is not an optimization detail in a TSDB, it is a load-bearing design goal, because it is the difference between keeping a week of data and keeping a year for the same money. The techniques come from Facebook's Gorilla paper and exploit two properties of the data. First, timestamps in a series are usually regularly spaced, for example every 15 seconds. Storing each 64-bit timestamp raw is wasteful, so instead you store the delta between consecutive timestamps, and then the delta-of-delta, the change in the delta. For perfectly regular sampling the delta-of-delta is zero, which encodes into a single bit, and small jitter encodes into a handful of bits, so a long run of evenly spaced timestamps collapses to almost nothing. Second, consecutive values within a series usually change little, and many metrics are near-constant or slowly trending. Gorilla encodes each floating-point value by XORing it with the previous value; when values are identical the XOR is zero (one bit), and when they are close the XOR has long runs of leading and trailing zero bits, so you store only the meaningful middle bits plus a short header describing where they sit. Together these took production data from 16 bytes per point down to about 1.37 bytes, roughly a 12x reduction. The one caveat is that this bit-packed encoding is designed for in-order, append-only writes within a block, which is another reason TSDBs prefer near-now, ordered ingestion and treat out-of-order backfill as a special case. Counters and gauges compress differently in practice, but the timestamp and value encoders above are the core of why a TSDB can hold so much history so cheaply.
Downsampling, rollups, and retention (raw to 1m to 1h)
Raw high-resolution data is expensive to keep and expensive to query over long ranges, so a TSDB uses two coordinated mechanisms to manage its lifecycle: downsampling and retention. Downsampling precomputes lower-resolution versions of the data. A background rollup job reads raw points and produces, for each series and each coarser bucket (say one minute, then one hour), a small set of aggregates: typically min, max, sum, count, and last, and often a percentile sketch, so that later queries can compute averages, rates, and approximate percentiles without the raw points. This is what lets a query over the last year read a few thousand hourly rollup points per series instead of billions of raw points, keeping long-range dashboards fast. Retention then enforces how long each resolution tier lives, for example raw for 15 days, 1-minute rollups for 90 days, and 1-hour rollups for 2 years. Because data is partitioned into time-bounded blocks, retention is a cheap operation: you delete whole expired blocks rather than scanning and deleting individual rows. Two subtleties matter in an interview. First, aggregation choice must be correct: you cannot average pre-averaged data across buckets, which is why rollups store sums and counts (so you can recombine correctly) and why percentiles require mergeable sketches rather than a stored p99 value. Second, downsampling is lossy by design, so the trade is always between paying to keep raw data for ad hoc high-resolution investigation and paying much less to keep only rollups, and mature systems let you tune that per metric.
Indexing tags with an inverted index and the query/aggregation engine
Because queries select series by tag matchers rather than by primary key, a TSDB needs an index that turns a tag predicate into a set of series quickly, and the right structure is an inverted index borrowed from search engines. For every label key-value pair (service=checkout, status=500, region=eu-west) the index keeps a posting list, the sorted set of series ids that carry that pair. A query such as service=checkout and status matches 5xx resolves by looking up the posting lists for the matching pairs and intersecting or unioning them to produce the exact set of series to read, so the engine never scans sample data for series it does not need. Once the series set is known, the query engine reads the relevant time range from the head block and any overlapping on-disk blocks, choosing the coarsest resolution tier that satisfies the requested step to minimize work. On top of the raw samples it applies time-series-native operations that a generic SQL engine does not have built in: group by time to bucket points into fixed intervals, rate and increase to convert monotonically increasing counters into per-second rates while correctly handling counter resets, aggregations like sum, avg, min, max across series, and quantile functions to compute percentiles, ideally from mergeable sketches so they can be combined across shards and rollups. In a sharded deployment this runs as scatter-gather: the coordinator sends the query to every shard owning matching series, each shard computes a partial aggregate locally to minimize data transfer, and the coordinator merges the partials into the final result.
Replication, durability, and push versus pull ingestion
Durability and availability in a distributed TSDB come from two layers. Within a node, the write-ahead log protects buffered in-memory data against a crash, so an acknowledged point survives a restart via WAL replay. Across nodes, series are replicated to multiple machines in different failure domains so that losing a node does not lose data or block writes; a common approach is to hash each series to a set of replica nodes and either write to a quorum of them or, as some systems do, run identical independent ingesters and deduplicate at query time. That last pattern connects to the biggest ingestion design choice: push versus pull. In a push model (InfluxDB, Graphite, OpenTSDB) clients send their metrics to the database, which is flexible for short-lived jobs and event-like data but makes the database a target for uncontrolled load and makes it harder to know whether a silent client is down or just idle. In a pull model (Prometheus) the database scrapes a metrics endpoint on each target at a fixed interval, which gives the server control over rate and timing, makes target health explicit (a failed scrape is an immediate signal), and makes it trivial to run multiple independent scrapers for redundancy, but it struggles with targets that are not directly reachable or are too short-lived to be scraped, which is why pull systems add a push gateway for batch jobs. Neither is strictly better: pull favors reliability and operational clarity in a controlled network, push favors flexibility and heterogeneous or ephemeral producers, and large platforms often support both.
Trade-offs to discuss
Every senior interviewer expects you to surface at least 3 of these. Pick the decisions, state the alternatives, and justify your choice.
Push ingestion versus pull ingestion
Pull (the server scrapes targets on a schedule) gives the database control over load, makes target liveness an explicit signal, and makes redundant scrapers trivial, but it cannot easily reach targets behind NAT or capture short-lived jobs without a push gateway. Push (clients send data in) handles ephemeral jobs and event-like data naturally and works through any network path, but it exposes the database to uncontrolled write bursts and turns is the client alive into an ambiguous question. The choice tracks your environment: pull for controlled, service-oriented fleets, push for heterogeneous or ephemeral producers.
Specialized columnar/append-only engine versus a general LSM store
A purpose-built engine with per-series columnar layout and delta/XOR encoding achieves far better compression and query efficiency for this exact workload, but it is bespoke code you must build and maintain. Reusing an LSM (Cassandra-style) gives you a proven, general engine and existing operational knowledge quickly, at the cost of weaker time-series compression and layout. Dedicated TSDBs almost always pick the specialized engine because the ~10x storage win pays for the engineering.
Tag cardinality versus query flexibility
Every tag you add multiplies the number of series, so rich tagging gives you more dimensions to slice and aggregate on, but each high-cardinality tag pushes memory and index size toward a cliff. Keeping tags low-cardinality (bounded, enumerable values) keeps the system stable and cheap but limits how finely you can query. The discipline is to reserve tags for dimensions you genuinely aggregate across and to push unbounded identifiers into logs or traces, accepting less query flexibility in exchange for a system that does not fall over.
Downsampling/rollups versus keeping raw data
Downsampling makes long-range queries fast and long-term storage cheap, but it is lossy: once raw points are dropped you can no longer investigate a spike at full resolution or recompute an aggregate you did not precompute. Keeping raw data preserves every future question at a large and growing storage cost. Mature systems compromise with tiered retention (short raw window, long rollup window) and store mergeable aggregates (sums, counts, sketches) so rollups remain correct for averages and percentiles.
Strict in-order, near-now writes versus supporting out-of-order and backfill
Optimizing for in-order, recent writes lets the head block, WAL, and bit-packed compression stay simple and fast, which is where the throughput and compression wins come from. Supporting out-of-order arrival and historical backfill (late-arriving IoT data, replays, migrations) requires extra buffering, more complex block handling, and often reduced compression efficiency. Many TSDBs historically rejected old data outright and only added bounded out-of-order support later, trading ingestion simplicity for real-world flexibility.
Single-node simplicity versus horizontal sharding
A single node with local disk is simple to operate, keeps all of a series data together for fast queries, and is often enough for millions of series, but it is capped by one machine's memory (which cardinality consumes) and one machine's failure domain. Sharding series across nodes scales cardinality and throughput and adds replication for availability, but it introduces scatter-gather queries, cross-shard aggregation, rebalancing, and deduplication complexity. The right point on this line is set by your active-series count and availability requirements, not by point rate alone.
Strong durability per write versus high ingestion throughput
Acknowledging each point only after it is replicated to a quorum and fsynced maximizes durability but adds latency and caps throughput, which is painful when you ingest millions of points per second. Buffering and batching writes, relying on the WAL plus periodic replication, and tolerating the loss of a sub-second window on a catastrophic multi-node failure trades a sliver of durability for a large throughput gain. Because metrics are usually high-volume and individually low-value (one lost 15-second sample rarely matters), most TSDBs lean toward throughput, which is a very different stance than a transactional database would take.
How Time-Series Database (a purpose-built store for timestamped metrics) actually does it
The design above follows the lineage of the systems that defined this space. Facebook's Gorilla, published at VLDB in 2015, is the canonical source for the compression and in-memory design: it introduced delta-of-delta timestamp encoding and XOR-based float encoding, reporting compression from 16 bytes per point down to about 1.37 bytes, and it made the case that keeping recent data in memory for fast reads is worth the RAM. Prometheus is the reference for the pull-based ingestion model and the WAL-plus-head-plus-immutable-blocks storage engine, and its documentation is explicit that it is built for reliability and near-now data rather than for durable long-term storage or backfill, which is why long-term storage is delegated to remote-write backends like Thanos, Cortex, or Mimir that add sharding, replication, and downsampling on top. InfluxDB's TSM engine and TimescaleDB's hypertables show the two ends of the build-versus-adapt spectrum: InfluxDB built a bespoke time-series engine, while TimescaleDB added automatic time partitioning and columnar compression to PostgreSQL, proving both that a relational base can be adapted and that doing so requires exactly the time-partitioning and compression additions a native TSDB has by default. An honest interview answer keeps two things front of center that all these sources agree on: cardinality, not point rate, is the real scaling limit, and compression plus time partitioning are what make the economics work. Any claim that you can freely add high-cardinality tags, or that a TSDB should update history like a relational database, contradicts how every one of these systems is actually built.
Lessons to study before this interview
If any of these topics are fuzzy, the interviewer will catch it. Each lesson is 15 to 60 minutes with diagrams, code, and a quiz.
Time-Series Databases
intermediate / database types storage
LSM Trees
intermediate / database types storage
Columnar Storage
intermediate / database types storage
Data Compression
advanced / consistency models
Range Partitioning
foundation / database fundamentals
Data Retention Policies
intermediate / data governance compliance
Time-Series Metrics
intermediate / observability monitoring
Related system design interview questions
Practice these next. They lean on the same core building blocks as Time-Series Database (a purpose-built store for timestamped metrics).