Ask an engineer to design a database and they reach for the tools they know: rows, a B-tree index, a mix of reads, updates, and deletes on individually addressable records. That instinct is correct for most systems and wrong for this one. A time-series database is built around a workload so lopsided that almost every general-purpose assumption inverts. Writes arrive constantly and in enormous volume, they almost always carry a timestamp near now, and once a point lands it is essentially never updated. Reads are heavily skewed toward recent data. Queries are range scans and time-bucketed aggregations, not point lookups. The whole design bends around those facts, and every interesting decision in it falls out of taking them seriously.
This is the long version. We will start from the shape of the workload, build up the series data model and why it matters, walk the write path from the write-ahead log through the in-memory head to immutable on-disk blocks, take apart the compression that makes months of history affordable, be precise about the one failure mode that actually kills these systems in production, and then work through downsampling, retention, and the query engine that turns a tag filter into a fast aggregation. By the end you should be able to defend, in front of a skeptical staff engineer, why a row store struggles here and where a purpose-built engine earns its keep. One note on numbers up front: every throughput, cardinality, and ratio figure here is an industry-typical range or an illustration, with one exception. The Gorilla figures of roughly 1.37 bytes per point and about a 12x reduction come from Facebook's published paper, and I cite them as such.
The workload is the whole argument
Start with what the data actually looks like. A point is tiny: a timestamp and usually a single float, sometimes with a little metadata. A single fleet of servers or IoT devices can push millions of these per second, each one describing CPU usage, request latency, a sensor reading, or a price at an instant. The points arrive in a relentless stream, almost always in timestamp order, and they are immutable once written. Nobody goes back and edits the CPU reading from four minutes ago.

Time-series workloads are append-heavy on writes, hot on recent data for reads, and immutable once a point lands.
Reads have their own bias. Dashboards refresh every few seconds and query recent windows, so the vast majority of read traffic touches the newest slice of data. When someone does ask a long-range question, they almost never want every raw point; they want a trend, a rate, a percentile bucketed into intervals. The dominant read is not "give me this one value" but "give me p99 latency per service over the last six hours, grouped into one-minute buckets," answered in well under a second.
Hold those three facts together, because the rest of the design is just their consequences. Writes vastly outnumber and outpace reads. Data is immutable. Access is overwhelmingly recent and aggregated. A storage engine tuned for exactly this can make choices a general database cannot, and that is the entire reason these systems exist as a separate category.
The series is the unit, and cardinality is the price
The fundamental object in a time-series database is not a row, it is a series. A series is identified by a metric name plus a set of key-value tags, and each series is an ordered stream of points. So http_requests_total with tags service=checkout, region=us-east, and status=500 is one series, and changing any tag value gives you a different one. This model is powerful because it lets you slice and aggregate a metric along any labeled dimension at query time. It is also a loaded gun, and understanding why is the difference between someone who has operated these systems and someone who has only read about them.

A series is a metric name plus a set of tags; the number of distinct tag combinations is the cardinality, and it is the real scaling limit.
The number of active series is called cardinality, and it equals the product of the cardinalities of all the tags on a metric. A metric with a service tag of 50 values and a region tag of 10 values has 500 series, which is nothing. Add an endpoint tag with 200 values and you are at 100,000. Now attach a tag whose values are unbounded, a user id, a full URL with query parameters, a request id, a container name that changes on every deploy, and cardinality does not grow, it explodes, because every new value mints a brand-new series that must be tracked for the whole retention window. Keep this word in mind. We will come back to it as the central failure mode, because it is the one thing this workload punishes hardest.
Why a relational row store fights you
Before building the specialized thing, it is worth being precise about why the general thing struggles, because the answer names every design decision that follows.
A relational engine stores data row by row and indexes it with a B-tree. That layout interleaves the timestamp, the value, and every tag column together on disk. A query that only wants the value over a time range still drags every other column through memory and cache, wasting I/O on data it will discard. The B-tree itself suffers under sustained high-rate inserts, because a stream of new series and new points keeps touching and rewriting index pages, producing write amplification and fragmentation exactly when you need ingestion to stay cheap. Values are stored in fixed-width, uncompressed form, which leaves enormous compression on the table, and we are about to see just how compressible this data really is. And retention in a row store means deleting billions of individual rows, which generates dead tuples, vacuum pressure, and more index churn, rather than simply dropping a file.
You can bolt time-series behavior onto a relational engine. TimescaleDB does precisely this, turning a PostgreSQL table into a hypertable that automatically partitions into time-bounded chunks and adds columnar compression on older chunks. That it works is real proof the relational base can be adapted. That it requires exactly time partitioning, columnar layout, and compression to be added on top is also the whole argument for why a native design has those properties from the start.
The write path: WAL, head block, immutable blocks
Here is the pattern at the heart of nearly every modern time-series database, and it is worth internalizing because it recurs everywhere. Recent data lives in fast, mutable memory. Historical data lives in tightly packed, read-only, time-bounded files. The write path is the machinery that moves data from the first state to the second with a durability guarantee in between.
