Bonus J
Time-Series Databases
Some data doesn't arrive. It accumulates — relentlessly, forever, one timestamp at a time.
Storing it like ordinary rows is how monitoring systems run out of disk on a Tuesday.
This is the last unwritten bonus in the series, and it's here because the series kept generating the exact problem it solves without stopping to name the tool.
Part 12 built an observability discipline on metrics and traces and noted, in passing, that "metrics are time-series data… storing millions of measurements per second is its own database problem." Bonus H's store table listed a time-series row and deferred it. Part 14's status log and Part 17's playback-quality metrics were both, quietly, streams of timestamped measurements.
Four times the series has pointed here. Part 20, next, designs analytics infrastructure that runs on this kind of data. So before we build the pipes, we name the tank: when your data is fundamentally "measurements over time," a general-purpose database is the wrong tool, and there's a category built for exactly this.
The Simple Mental Model: A Ship's Logbook vs a Filing Cabinet
A filing cabinet — the relational database (Blog 5) — is built for records you look up, update, and cross-reference. Pull a customer's folder, change their address, file it back. Records are mutable, related, and queried by identity.
A ship's logbook is a different object entirely. Every entry is stamped with a time and written on the next empty line. Nobody goes back and edits yesterday's noon position. Nobody deletes a single mid-voyage line. You append at the bottom, forever, and you read it by span — "what happened between Tuesday and Thursday," never "fetch entry #4,472 and change it."
Time-series data is a logbook, not a cabinet: append-mostly (new measurements arrive, old ones are never updated), time-ordered (the timestamp is the primary axis of everything), queried by window ("the last hour," "this range," not by individual key), and aged out wholesale (old entries expire together, by policy, not one at a time).
Try to run a logbook inside a filing cabinet and everything the cabinet is good at — updating, indexing by identity, relating records — becomes overhead you're paying for and never using. That mismatch is the whole reason this category exists.
What Time-Series Data Actually Is
Time-series data is a sequence of values, each bound to a timestamp, usually tagged with a source. The shape is strikingly consistent across wildly different domains:
timestamp metric tags value
2026-07-10T14:02:11Z cpu_utilization host=web-07,region=in 73.2
2026-07-10T14:02:11Z orders_per_sec service=checkout 41
2026-07-10T14:02:12Z temperature sensor=warehouse-3 4.1
You have already met this data all over the series, wearing different clothes: metrics and monitoring (Part 12's CPU, latency, queue depth, cache hit rate — every dashboard line is a time series), application/infrastructure telemetry (the vital signs of every system in Parts 6–19), IoT and sensor readings (temperature, GPS, device state, streamed continuously), financial tick data (prices sampled thousands of times a second), user-activity and event streams (Part 14's notification status log, Part 16's engagement events), and playback and QoE metrics (Part 17's rebuffer ratio and bitrate over time).
The unifying properties, the ones that break normal databases: time is the primary axis — nearly every query filters, groups, or orders by time first. Writes vastly outnumber reads, and writes are almost always appends — updates are rare to nonexistent. Volume is enormous and continuous — not a spike, a firehose that never turns off. Recent data is hot, old data is cold — you query the last hour constantly and last year almost never. And data has a natural lifespan — most of it is worth keeping in full for days, in summary for months, and not at all beyond that.
Hold that last property. It's the one that quietly dooms the naive approach.
Why a Normal Database Struggles
Point a relational database (Blog 5) at a metrics firehose and it fails in ways that compound.
Write amplification from indexes. Every insert updates the table and its indexes. Relational databases are tuned for read-optimized indexing on mutable data, but here, millions of appends per second turn index maintenance into the bottleneck (Blog 5's B-tree, thrashing). You're paying, on every single write, for update-and-lookup machinery you never use.
Storage explosion without natural compression. Time-series data is deeply compressible — consecutive timestamps differ by tiny regular intervals, and consecutive values often barely change (CPU at 73.1, 73.2, 73.0…). A general store treats each row as independent and misses nearly all of it. The bill (Part 8) grows linearly with a firehose that never stops.
Deletion is brutal. Here's the property that dooms it. Retention means "drop everything older than 30 days," and in a relational database, deleting billions of old rows is a massive, expensive, index-churning operation that fights the live write load for resources (Part 6/8's lesson: deletion at scale is its own problem). You end up spending as much effort removing old data as storing new data.
Time-window queries scan too much. "Average CPU per minute over the last 6 hours across 500 hosts" is an aggregation over an enormous number of rows. Without time-aware storage layout, that's a full-range scan — slow, and competing with the write firehose for I/O.
No downsampling. Nobody needs per-second resolution for last year — minute or hour averages are fine. A general database has no native notion of "keep full detail recently, summaries long-term," so you store everything at full resolution forever, or hand-build the machinery yourself.
The through-line: a general-purpose database is built for mutable, related records queried by identity. Time-series data is immutable, independent measurements queried by time. Every mismatch is a cost you pay continuously.
What Time-Series Databases Do Differently
A time-series database (TSDB) is built from the ground up for the logbook shape. The optimizations map one-to-one onto the problems above.
Append-optimized storage. Writes are appends to time-ordered structures, not updates into indexed tables. This is the difference between writing on the next empty logbook line and filing a folder back into a sorted cabinet — one is essentially free, the other fights the index every time. TSDBs are built to ingest millions of points per second because appending is all they optimize for.
Aggressive time-aware compression. Because timestamps advance in regular steps and values change slowly, TSDBs use specialized encodings — storing deltas between consecutive timestamps and deltas between consecutive values rather than the raw numbers. The regularity that a general store ignores, a TSDB exploits, often achieving order-of-magnitude compression — this isn't a marketing number, it's a well-documented result of exactly this delta and delta-of-delta technique applied to real production metrics.
Retention as a first-class, cheap operation. This is the killer feature. Data is stored in time-bucketed chunks, so "delete everything older than 30 days" becomes "drop the old chunks," a cheap metadata operation, not a billion-row scan. The relational database's most brutal task becomes the TSDB's easiest one, because the storage was organized by time from the start.
Downsampling and rollups built in. TSDBs natively "keep 1-second resolution for 7 days, 1-minute for 30 days, 1-hour for a year." Old data is automatically summarized, not discarded, the exact tiering Part 8 taught for storage, applied to time. You get long history at a fraction of the storage.
Time-window queries as the primary path. The entire storage layout is organized around time ranges, so "last hour," "this range grouped by minute," and "rate of change over this window" are the fast path, not an afterthought, with time-native functions (rate, moving average, percentiles-over-time) built in.
The tags model for filtering. Measurements are tagged (host, region, service), and TSDBs index those tags so you can slice — "p99 latency by region over the last hour," efficiently. (With one sharp edge, coming up.)
The single sentence: a TSDB trades away the things time-series data doesn't need — updates, relational joins, per-row deletion — to do the things it does need — massive append throughput, time-window queries, compression, and retention — dramatically better.
When to Use a Time-Series Database — and When Not To
This is a bonus about judgment, so here's the decision, honestly (Bonus C's discipline: the requirement picks the tool, not the trend).
Reach for a TSDB when the data is fundamentally measurements over time — metrics, telemetry, sensor readings, events with timestamps. Writes are appends and vastly outnumber reads (or reads are almost always time-windowed aggregations). Volume is high and continuous — a firehose, not a trickle. Retention and downsampling matter — you need automatic aging and rollups. Queries are "over this time range, grouped/aggregated by time." Monitoring and observability (Part 12) is the textbook case — it's why the metrics half of observability quietly runs on TSDBs.
Don't reach for one when the data is relational and transactional — orders, users, inventory (Part 18). A TSDB has no place here; it can't do the joins, transactions, or per-record updates that data lives on. You need frequent updates or deletes of individual records — the logbook doesn't edit past lines. The data isn't really time-centric — a timestamp column does not make a table time-series, the question is whether time is the primary query axis. Volume is modest — a normal database handles low-rate timestamped data perfectly well, a TSDB is operational overhead you haven't earned (Bonus C's muscles, another store to run). You need strong transactional guarantees across measurements — not the TSDB's job.
The honest framing, echoing Bonus H: a TSDB is a specialized tool in a polyglot system, not a replacement for your primary database. It sits alongside the relational source of truth, ingesting the measurement firehose the main database should never see. Most real systems that use one use it for exactly one thing — observability, IoT, or analytics — while orders and users stay in the relational store where they belong.
The Sharp Edge: Cardinality
One failure mode deserves its own warning, because it's the way teams actually blow up a TSDB, and it's the same disease the series has met before.
TSDBs index by tags. Cardinality is the number of unique tag combinations — every distinct (host, region, service, …) tuple is a separate series the database must track.
Tag by region (a handful of values) and service (dozens): manageable. Tag by user_id or request_id (millions of values): catastrophe. Each unique value multiplies the series count, and high cardinality explodes memory and index size until the database falls over.
This is exactly Part 12's cardinality warning — "high-cardinality dimensions are gold in logs and traces, a bomb as metric labels" — now explained at the storage layer. The reason that rule exists is this: metric labels become TSDB tags become series count, and unbounded series count is how you kill a time-series database.
The rule: tags are for low-cardinality dimensions you slice by; unbounded identifiers belong in logs and traces (Part 12), never as TSDB tags.
How This Fits the Series
Time-series databases aren't a detour — they're the store under machinery you've already built. Part 12 (Observability) runs its metrics on this — every dashboard line, every SLO, every "p99 over the last hour" is a TSDB query, and this bonus is the storage engine beneath that entire article. Part 8 (Storage) taught tiering — hot/warm/cold, lifecycle policies — and TSDB downsampling is that tiering, applied to time: full resolution hot, rollups warm, dropped cold. Bonus H (Polyglot Persistence) predicted this exactly — the right store per data shape; a TSDB is a derived, specialized store fed by the measurement firehose, your relational database remains the source of truth. Part 7 (Messaging) is often the pipe — telemetry flows through a stream, into the TSDB, in the same async pattern as everything else downstream of the hot path. And Part 20 (next) designs analytics infrastructure that ingests and serves exactly this kind of data at scale.
A time-series database is Part 8's storage tiering, Bonus H's polyglot discipline, and Part 12's metrics, fused into one specialized tool for the shape of data that only ever moves forward in time.
Common Mistakes Engineers Make with Time-Series Data
Storing metrics in the primary relational database — the firehose that thrashes the indexes and competes with real transactional load, until retention deletion finishes the job. Assuming a timestamp column makes data "time-series" — the test is whether time is the primary query axis, not whether a date is present. High-cardinality tags — user_id or request_id as tags, the series explosion that kills the database (Part 12's rule, at the storage layer). No retention policy — a firehose with no drain, infinite growth, guaranteed. No downsampling — storing per-second resolution forever when minute or hour summaries would do, paying full price for history nobody queries at full detail. Using a TSDB for relational/transactional data — no joins, no transactions, no per-record updates, the wrong tool, chosen for novelty. Adding a TSDB you don't need — modest timestamped volume in a normal database is fine, a second store is operational cost (Bonus C's muscles) you must justify. Ignoring it when you clearly do need it — cramming a genuine metrics firehose into a relational store because "we already have one," the false economy that ends in a Tuesday outage.
From My Journey: An Architectural Lesson
Time-series data has a way of sneaking up on a system. It rarely arrives as a requirement anyone writes down. It shows up as monitoring — a few metrics, stored in whatever database is already running. Then more metrics. Then per-host, per-endpoint, per-region breakdowns. Then someone adds a dashboard, and another, and the "few metrics" have quietly become millions of points a minute flowing into a database that was designed to hold customer records.
The failure, when it comes, is confusing at first, because nothing is logically wrong — the data is fine, the queries are correct. It's just that the database is drowning: writes slowing, storage ballooning, and the retention job locking things up every night as it tries to delete a mountain of old rows one at a time.
The realisation that reorganizes it: this data was never the same kind of data as everything else in the system. Orders and users are records — mutable, related, queried by identity. Metrics are measurements — immutable, independent, queried by time and thrown away on a schedule. They look similar because both have rows and both have timestamps, but they want opposite things from a database, and forcing them into one store means one of them suffers.
And it's Bonus H's lesson, arriving through the back door. The question was never "which database" — it was "which kind of data is this, and does it deserve its own store?" Measurements-over-time, at volume, is one of the clearest "yes, its own store" answers in all of system design.
The lesson that stuck: not all data is records. Some data is just the relentless forward march of measurement, and it deserves a tool built for time.
Key Takeaways
- Time-series data is append-mostly, time-ordered, queried by window, and aged out wholesale — a logbook, not a filing cabinet.
- You've met it everywhere: Part 12's metrics, Part 14's status logs, Part 17's QoE, plus IoT, finance, and activity streams.
- Relational databases struggle on all four axes: index write-amplification, no natural compression, brutal bulk deletion, and slow time-window scans.
- TSDBs invert every trade-off: append-optimized writes, delta-based compression, chunk-drop retention, native downsampling, and time-window queries as the fast path.
- Retention is the killer feature: "drop old chunks" is a metadata operation, not a billion-row delete.
- Downsampling is Part 8's storage tiering applied to time — full resolution hot, rollups warm, dropped cold.
- Cardinality is the sharp edge: high-cardinality tags (user_id, request_id) explode series count and kill the database — Part 12's rule, at the storage layer.
- Use a TSDB for measurements-over-time at volume; never for relational/transactional data — it sits alongside the source of truth, not instead of it (Bonus H).
- Adding one is a Bonus C decision: another store to operate, justified only when the data shape genuinely demands it.
Interview Lens
Time-series rarely gets a dedicated interview question, which is why raising it in an observability, IoT, or analytics design signals depth.
The trigger to recognize: the moment a design involves "store metrics," "sensor data," "track events over time," or "monitoring at scale," the strong candidate names the data shape: "This is time-series data — append-heavy, queried by time window, with retention. I'd put it in a time-series database rather than the primary store, so the metrics firehose never touches the relational database that holds the transactional data."
Expect the probes: "Why not just use your main database?" — write amplification, no compression, brutal retention deletion, slow window scans; name the retention problem specifically, it's the most concrete. "What's the risk with a TSDB?" — cardinality; high-cardinality tags explode series count, that's the killer, and it connects straight back to the metrics-vs-logs labeling rule. "When would you NOT use one?" — relational/transactional data, modest volume, or when time isn't the primary query axis; knowing when not to is the senior signal.
The differentiator: framing the TSDB as one specialized store in a polyglot system (Bonus H), alongside the relational source of truth, fed by a stream (Part 7), serving observability (Part 12). That's the whole series clicking together in one answer.
Real-World Engineering Lens
Check where your metrics actually live. If application or infrastructure metrics are in your primary transactional database, you have a firehose competing with real load, and a retention job that gets worse every month. That's a migration worth scoping.
Audit your tag cardinality before it bites. List the tags on your highest-volume metrics; any tag that can take millions of values (user, request, session) is a series-explosion risk. Move those dimensions to logs and traces (Part 12).
Set retention and downsampling from day one. A TSDB without a retention policy is a disk-space outage on a timer. Decide hot/warm/cold windows when you provision it, not when it fills.
Don't reach for a TSDB reflexively. Modest timestamped volume in your existing database is fine. The second store is real operational cost (Bonus C), justify it with the data shape and the volume, not the novelty.
Treat it as derived, feedable, and rebuildable (Bonus H) — the source of truth for business facts stays in your primary store; the TSDB serves the measurement layer beside it.
What's Next
We've now named every specialized store the series leans on — relational (Blog 5), cache (Part 6), object (Part 8), search and analytics (Bonus H), and now time-series. The measurement layer has a home.
Which sets up the finale of Section 3, the layer underneath all of it. We've used distributed queues, object storage, and analytics pipelines for nineteen articles, treating them as reliable substrate. Now we design that substrate from the inside.
Blog 20 — Infrastructure Systems: Distributed Queues, Storage, and Analytics. How the message brokers, object stores, and data pipelines — including the ingestion path that feeds a time-series database — are actually built to survive machine death and never lose what they're trusted to hold.
Question for Readers
Look at where your system stores its metrics and monitoring data right now.
Is it a database built for measurements over time, or is a firehose of timestamps quietly competing with your real business data for writes, storage, and that nightly retention job?
Most teams don't check until the disk fills. The logbook was never meant to live in the filing cabinet.