TimescaleDB and InfluxDB are the two names most teams shortlist when metrics, sensor readings or events outgrow a plain table. They are built on opposite premises. TimescaleDB is a PostgreSQL extension: your time series live in ordinary SQL tables, partitioned and compressed behind the scenes, next to the rest of your relational data. InfluxDB is a purpose-built time-series database with its own write protocol, its own data model of measurements, tags and fields, and, in its current generation, a columnar engine that writes Parquet files to object storage.
A fair comparison has to name versions, because InfluxDB is really three products. InfluxDB 1.x and 2.x use the TSM storage engine with a series index, and 2.x added the Flux language. InfluxDB 3, generally available since 2025 as the open-source Core and the commercial Enterprise, was rebuilt on Apache Arrow, DataFusion and Parquet, queries with SQL and InfluxQL, and does not run Flux. TimescaleDB, now developed by TigerData (the company renamed itself from Timescale in June 2025), has evolved more gradually. This article compares their designs, walks through the same workload on both, and ends with how to choose.
Two architectures
A TimescaleDB hypertable looks like one table to your queries but is stored as many child tables called chunks, each covering a time range and optionally a space partition. Inserts go through the normal PostgreSQL path: write-ahead log, shared buffers, heap pages and btree indexes. Queries that filter on time touch only the chunks that overlap the range, which is the first big win over a plain table. As chunks age, a policy converts them to the columnstore, where rows are grouped into batches, stored column by column and compressed. Dropping old data means dropping whole chunks, which is instant compared with a large DELETE.
InfluxDB 3 accepts writes as line protocol over HTTP, appends them to a write-ahead log on object storage or local disk on a short flush interval, and keeps recent data in memory as Arrow record batches that are queryable immediately. It periodically persists data as Parquet files organised by table and time. Queries run on DataFusion, a vectorised SQL engine, across the in-memory buffer and the Parquet files. Enterprise can run separate ingest, query and compaction nodes over the same object store.
The older InfluxDB engine, TSM, is different again: a log-structured design that writes a WAL and cache, flushes immutable TSM files and compacts them, much like the LSM trees described in LSM compaction, with a separate index mapping each series key to its data.
Data model and schema
In TimescaleDB you design a table. A common choice is a narrow table with a time column, an entity identifier and one column per metric, or a key-value table with a metric name column when metrics vary. You get foreign keys, joins to a devices table, JSONB for irregular attributes and every PostgreSQL type.
CREATE TABLE readings (
time TIMESTAMPTZ NOT NULL,
device_id INTEGER NOT NULL REFERENCES devices(id),
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
battery REAL
);
SELECT create_hypertable('readings', by_range('time', INTERVAL '1 day'));
CREATE INDEX ON readings (device_id, time DESC);InfluxDB organises data into databases (buckets in 2.x), tables (measurements), tags and fields. Tags are strings that identify a series (indexed in 1.x and 2.x, plain columns in InfluxDB 3), fields are the measured values, and every point has a timestamp. In InfluxDB 3 a table's tags and fields become columns in Arrow and Parquet. You do not declare the schema in advance; the first write creates the table and later writes can add columns.
readings,device=d-0042,site=plant-7 temperature=21.4,humidity=0.43,battery=3.71 1759390800000000000
readings,device=d-0043,site=plant-7 temperature=22.1,humidity=0.40,battery=3.65 1759390800000000000The modelling rule that matters most is the same in both: put identifying, low-to-moderate cardinality attributes in tags or indexed columns, put measurements in fields or value columns, and never put unbounded values such as request IDs or user IDs in tags.
Write paths and ingest
TimescaleDB ingest is PostgreSQL ingest. Throughput comes from batching: multi-row INSERT statements or COPY, a connection pool, and keeping the number of indexes on the hypertable small, because each index costs a btree update per row. Out-of-order and late data is not a special case; it lands in whatever chunk covers its timestamp. Upserts with ON CONFLICT need a unique index that includes the time column.
InfluxDB ingest is batched line protocol over HTTP. InfluxDB 3 exposes its own write endpoint and also accepts the 1.x and 2.x write APIs for compatibility, so existing Telegraf agents and client libraries keep working. A write is acknowledged after the WAL flush. Because there is no transaction across tables and no foreign keys, ingest is simpler to scale, and a duplicate point with the same table, tag set and timestamp replaces the earlier field values rather than creating a second row.
# Batched writes to InfluxDB 3 with plain HTTP (v2-compatible endpoint shown).
import requests, time
def flush(points, url, db, token):
body = "\n".join(points)
r = requests.post(f"{url}/api/v2/write", params={"bucket": db, "precision": "s"},
headers={"Authorization": f"Token {token}"}, data=body, timeout=10)
if r.status_code >= 500 or r.status_code == 429:
raise RuntimeError("retry with backoff") # transient: keep the batch
r.raise_for_status() # 4xx: a bad line, do not retry blindly
batch = [f"readings,device=d-{i:04d} temperature=21.0 {int(time.time())}" for i in range(5000)]
Query paths
TimescaleDB queries are PostgreSQL queries plus functions such as time_bucket, first, last and gap filling. The planner excludes chunks outside the time range, and for columnstore chunks it decompresses only the columns and segments it needs. The strength is everything around the time-series part: joins to metadata, window functions, CTEs, and the whole PostgreSQL ecosystem of drivers, ORMs and BI tools.
SELECT time_bucket('5 minutes', r.time) AS bucket,
d.site,
avg(r.temperature) AS avg_temp,
max(r.temperature) AS max_temp
FROM readings r
JOIN devices d ON d.id = r.device_id
WHERE r.time > now() - INTERVAL '6 hours'
AND d.site = 'plant-7'
GROUP BY bucket, d.site
ORDER BY bucket;InfluxDB 3 queries use SQL through DataFusion, with date_bin for bucketing, or InfluxQL for compatibility with 1.x dashboards. Clients can use the HTTP API or Arrow Flight, which streams results as Arrow batches without row-by-row serialisation. Joins exist in the SQL engine, but your metadata usually lives elsewhere, so in practice enrichment happens in the application or the dashboard. InfluxDB 2.x users with Flux scripts have to rewrite them in SQL or InfluxQL when moving to 3.
SELECT date_bin(INTERVAL '5 minutes', time) AS bucket,
site,
avg(temperature) AS avg_temp,
max(temperature) AS max_temp
FROM readings
WHERE time > now() - INTERVAL '6 hours' AND site = 'plant-7'
GROUP BY bucket, site
ORDER BY bucket;
Cardinality
Cardinality, the number of distinct series, is where InfluxDB's history matters. In 1.x and 2.x every unique combination of measurement and tag values is a series key in an index, and memory and startup time grow with the series count. Workloads with millions of short-lived series, such as per-container or per-request tags, were a well-known way to hurt TSM.
InfluxDB 3 removed that index. Tags are columns, and high-cardinality values are just more distinct values in a column. That does not make cardinality free: it affects sort order, Parquet file pruning and compression, and queries that group by a high-cardinality tag still produce many groups. TimescaleDB never had a series index; a high-cardinality device_id is a column with a btree index, and the cost shows up as index size and, in the columnstore, as many small segments if you segment by it.
Compression, retention and rollups
TimescaleDB's columnstore, called hypercore in current documentation, groups rows by a segmentby column and orders them within segments by an orderby column. Choose segmentby as the column you filter on most, usually the entity ID, and orderby as time descending. Then add a policy that converts chunks after they stop receiving most writes:
ALTER TABLE readings SET (
timescaledb.enable_columnstore = true,
timescaledb.segmentby = 'device_id',
timescaledb.orderby = 'time DESC'
);
CALL add_columnstore_policy('readings', after => INTERVAL '7 days');
CREATE MATERIALIZED VIEW readings_hourly
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', time) AS bucket, device_id,
avg(temperature) AS avg_temp, max(temperature) AS max_temp
FROM readings GROUP BY bucket, device_id;
SELECT add_continuous_aggregate_policy('readings_hourly',
start_offset => INTERVAL '3 days', end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour');
SELECT add_retention_policy('readings', drop_after => INTERVAL '90 days');Continuous aggregates are materialized views that refresh incrementally, recomputing only buckets whose source data changed. They are TimescaleDB's best feature for dashboards: keep raw data for weeks, hourly rollups for years. Compare with plain PostgreSQL in materialized views in Postgres. Check the edition: the core of TimescaleDB is Apache 2.0, while the columnstore, continuous aggregates and policies are in the community edition under the Timescale License. A PostgreSQL host that ships only the Apache build will not have them.
InfluxDB 3 gets columnar compression from Parquet without configuration, and the object store makes long retention cheap. Rollups are done by the processing engine, which runs Python plugins on writes or on a schedule, or by an external job that queries and writes back. InfluxDB 2.x used Flux tasks for this. The important limitation in InfluxDB 3 Core is that it has no compactor. Every query is capped by the number of Parquet files it may read, 432 by default, which corresponds to roughly 72 hours of data at default settings and can be raised with the --query-file-limit option at the cost of memory and object-store requests. Core is designed for recent data; long-range history queries are what Enterprise, with compaction, is for.
Worked sizing example
Take 10,000 devices reporting five metrics every 10 seconds. That is 1,000 rows per second in a wide TimescaleDB table, or 5,000 field values per second, 86.4 million rows a day. In row storage a row of a timestamp, an integer and five numbers costs roughly 70 to 100 bytes including tuple header and index entries, so 6 to 9 GB a day before compression. In the columnstore, slowly changing sensor values compress very well; vendor documentation claims up to 98 percent for favourable data, and you should measure your own with a day of real readings, not assume it.
The same data in InfluxDB 3 is 1,000 lines per second, trivially within one node's ingest. Parquet with dictionary-encoded tags and delta-encoded timestamps usually lands in the same range as the TimescaleDB columnstore. The deciding numbers are elsewhere: how far back your dashboards look, which matters for Core's file cap; how many rollups you need; and whether queries join to the devices table. Distrust vendor benchmarks; each is tuned to flatter its own design.
Operating each one
Running TimescaleDB is running PostgreSQL. You need connection pooling, vacuum tuning, backups with a tool such as pgBackRest or a managed service, replication for high availability, and upgrades of both PostgreSQL and the extension. The upside is that your team may already know all of this, and that one database can hold both relational and time-series data. Index design matters as much as for any table; see Postgres index strategies.
Running InfluxDB 3 means running a stateless-looking server backed by an object store, plus whatever replaces the features Core lacks: compaction, high availability and long-range queries come with Enterprise or the managed cloud offerings. Durability depends on the object store and the WAL flush. Monitoring pipelines often pair it with Telegraf; if your workload is pure infrastructure metrics scraped from services, also consider Prometheus, which neither of these replaces for alerting on scraped targets.
Failure modes
- TimescaleDB chunk interval too small, creating thousands of chunks and slow planning, or too large, so the active chunk and its indexes do not fit in memory.
- Converting chunks to the columnstore while they still receive heavy writes or updates, which makes those writes expensive.
- Real-time continuous aggregates behaving differently from what dashboards expect. Whether a query merges unmaterialized recent data depends on the materialized_only setting, whose default changed between releases; set it explicitly.
- InfluxDB tags with unbounded values, crippling TSM in 1.x and 2.x and still hurting pruning in 3.
- InfluxDB 3 Core queries failing past the Parquet file cap once dashboards ask for a week of data.
- Duplicate points silently overwriting each other in InfluxDB when two writers share a tag set and timestamp.
- Migrating InfluxDB 2.x without budgeting for rewriting Flux tasks and dashboards.
How to choose
| If you need | Lean toward | Because |
|---|---|---|
| Joins with relational data, transactions, SQL tooling | TimescaleDB | It is PostgreSQL |
| Incremental rollups maintained by the database | TimescaleDB | Continuous aggregates |
| Cheap long retention on object storage, separate compute | InfluxDB 3 Enterprise or cloud | Parquet on object store, compaction |
| Recent-data monitoring with Telegraf, open source | InfluxDB 3 Core | Simple ingest, recent-window design |
| Existing Flux code | Stay on 2.x or budget a rewrite | InfluxDB 3 does not run Flux |
| One database for the whole application | TimescaleDB | No second system to operate |
What to do next
- Write down the query mix: time windows, group-by keys, joins to metadata and retention per resolution.
- Estimate rows per second, series count and the maximum cardinality of every candidate tag or indexed column.
- Load one week of real data into both, using the schemas above, and measure compressed size and p95 latency for your top ten queries.
- For TimescaleDB, pick the chunk interval, segmentby and orderby, and check which edition your PostgreSQL host ships.
- For InfluxDB 3, test the longest dashboard range against Core's file cap and decide whether you need Enterprise.
- Design rollups first: continuous aggregates or processing-engine plugins, with retention for each level.
- Plan backups, high availability and upgrades with the team that will be on call.
- Read when to pick Postgres if the deciding factor is consolidating onto one database.