Skip to content
Go back

InfluxDB vs TimescaleDB vs VictoriaMetrics

By SumGuy 18 min read
InfluxDB vs TimescaleDB vs VictoriaMetrics
Contents

InfluxDB wins if your cardinality stays low and you want a purpose-built metrics store, TimescaleDB wins if you’re already running Postgres, and VictoriaMetrics wins the moment cardinality or storage cost gets out of hand.

You’ve got metrics pouring in from your infrastructure, and now you need a place to stuff them that won’t choke, bankrupt you, or require a PhD in query syntax. Three names keep popping up: InfluxDB, TimescaleDB, and VictoriaMetrics. They all claim to solve the time-series problem, but they solve it differently.

This isn’t a “pick the winner” article because there isn’t one. It’s a “which one fits your chaos” guide.

The Three Engines at a Glance

These are simply three different approaches to the same basic toolbox:

The metaphor: InfluxDB is a sports car optimized for the race track. TimescaleDB is a sedan that’s been tuned for highway driving. VictoriaMetrics is a motorcycle: light, fast, and it doesn’t care if the road is crazy.

Versions in this article, checked September 2026: InfluxDB 2.9.1 and 3.11.x, TimescaleDB 2.29.2, VictoriaMetrics v1.151.0.

InfluxDB: The Time-Series Native

InfluxDB was born for metrics. Every design decision assumes you’re shoving timestamps and numbers at it constantly, then querying the heck out of them in predictable patterns.

The Good

Ingestion speed is the first thing you notice. The storage engine is built for write-heavy workloads with predictable query patterns, so a batched line-protocol writer keeps up with far more points per second than a row-per-insert Postgres table ever will. I am not going to give you a number, and neither should anyone else. More on why below.

Retention is a first-class feature. Every bucket carries a retention period, and InfluxDB deletes anything past it on its own with no cron job to baby-sit.

Be careful what that actually buys you, though. In 2.x, bucket retention only deletes. It does not downsample. The pattern people describe as built-in retention, “keep raw data for 7 days, then keep 1-hour rollups for a year”, is two buckets plus a scheduled task that aggregates from one into the other. You write that task yourself:

downsample-task.flux
option task = {name: "cpu_1h", every: 1h}
from(bucket: "metrics")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "cpu_usage")
|> aggregateWindow(every: 1h, fn: mean)
|> to(bucket: "metrics_1h")

Two buckets, two retention periods, one task you have to monitor. Still better than cron, but it is not free.

The migration path off 2.x is real. InfluxDB 3 exists, it is a full Rust rewrite on Apache Arrow and Parquet, and it went GA with v3.0.0 in April 2025. As of September 2026 it is on 3.11.x, published as influxdb:3-core and influxdb:3-enterprise on Docker Hub. It drops the TSM/TSI index entirely, which is why InfluxData markets it as having no cardinality limit. If you are picking InfluxDB fresh today, pick 3.

The Bad

Cardinality is your enemy in 2.x. The TSM/TSI engine balloons memory and slows to a crawl as your series count climbs into the millions. There’s no single magic number where it hard-stops in OSS, but you’ll feel the pain long before then: ingest stalls, queries time out, and the process gets OOM-killed. That sounds like science fiction until you tag every request path plus HTTP method plus error code plus customer ID on a busy API, then wonder why metrics stop flowing at 2 AM.

Example: If you measure http_requests with tags for method, path, status, customer, and region, you’re creating a new series for every combination. 10 methods × 50 paths × 20 statuses × 500 customers × 3 regions = 15 million series. Your cluster catches fire.

Flux is a dead end. Flux is the functional query language that shipped with 2.x, and it is powerful if you like that style. InfluxData has since put it into maintenance mode, tells 2.x users to prefer InfluxQL, and did not carry it into InfluxDB 3 at all. You still need it for tasks like the downsampling example above, because that is the only scheduling mechanism 2.x has. Write your dashboards in InfluxQL and keep Flux confined to tasks.

Multi-tenancy is awkward. The unit of isolation is the bucket, and cardinality pressure is felt per bucket, so one noisy tenant in a shared bucket degrades queries for everyone in it. Split tenants into their own buckets and you get isolation, but you lose the ability to query across them in one shot, and every bucket carries its own retention and its own task plumbing. If you’re building a SaaS product where each customer is their own tenant, budget an afternoon for that decision and expect to live with it.

Cloud pricing is metered on four axes, not two. InfluxDB Cloud Serverless on the Usage-Based Plan charges for volume of data in at $0.0025/MB, query count at $0.012 per 100 query executions, storage at $0.002/GB-hour, and volume of data out at $0.09/GB. Those are InfluxData’s published rates as of September 2026.

The storage line is the one that catches people. $0.002 per GB-hour is roughly $1.46 per GB per month, so 200GB of retained metrics runs about $290 a month before you have written or queried a single point. Compare that against a $40 VPS with a 200GB volume before you decide hosted is the easy option. Self-hosted InfluxDB is free, but you’re running the database yourself.

A Typical Docker Compose Setup

docker-compose.yml
services:
influxdb:
# 2.9 is the current 2.x line. For a new deployment use 3-core instead.
image: influxdb:2.9
ports:
- "8086:8086"
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: admin
DOCKER_INFLUXDB_INIT_PASSWORD: changeme
DOCKER_INFLUXDB_INIT_ORG: your-org
DOCKER_INFLUXDB_INIT_BUCKET: metrics
volumes:
- influxdb_data:/var/lib/influxdb2
healthcheck:
test: ["CMD", "influx", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
influxdb_data:

The influx CLI ships inside the image, so that healthcheck actually resolves. Not every healthcheck you copy off the internet does, as the VictoriaMetrics section will demonstrate.

Ingesting with Python:

write_influx.py
import time
from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS
client = InfluxDBClient(url="http://localhost:8086", token="your-token", org="your-org")
write_api = client.write_api(write_options=SYNCHRONOUS)
point = Point("cpu_usage") \
.tag("host", "server-01") \
.tag("region", "us-west") \
.field("percent", 45.2) \
.time(int(time.time() * 1e9))
write_api.write(bucket="metrics", record=point)

TimescaleDB: PostgreSQL on Steroids

TimescaleDB is a PostgreSQL extension that wraps hypertables around your time-series data. If you’re already running Postgres, this is like finding out your car had a turbo the whole time.

One naming note before the code: the company behind it rebranded from Timescale to TigerData in 2025. The extension is still timescaledb, the Docker images are still timescale/timescaledb, but the docs now live at docs.tigerdata.com and the managed service is Tiger Cloud. If you search for old feature names you will land on pages using new ones.

The Good

SQL. Just SQL. No special query language, no learning curve beyond what you already know. Your entire team can write queries immediately. This matters more than it sounds.

Soft cardinality. There’s no hard limit. You can tag millions of unique dimensions. It gets slower, sure, but it doesn’t brick at a magic number.

Compression is excellent, once you turn it on. TigerData now calls the feature the columnstore and claims 90%+ compression as typical for time-series data. It is not on by default, and it is two statements to enable:

enable-columnstore.sql
ALTER TABLE metrics SET (
timescaledb.enable_columnstore = true,
timescaledb.segmentby = 'host',
timescaledb.orderby = 'time DESC'
);
CALL add_columnstore_policy('metrics', after => INTERVAL '7 days');

Note the CALL. In 2.29, add_columnstore_policy is a procedure, so SELECT add_columnstore_policy(...) fails with add_columnstore_policy(unknown, after => interval) is a procedure. The older add_compression_policy is still a function and still takes SELECT, which is exactly why copying a 2023 blog post half-works and then blows up on the second line. The old spellings (timescaledb.compress, timescaledb.compress_segmentby, add_compression_policy) all still function, but they are deprecated and scheduled for removal in the next major release.

Ecosystem integration. Since it’s Postgres, you get all the Postgres goodness: full-text search, JSON columns, PostGIS for geospatial data, full ACID semantics. Want to correlate metrics with log data? Put it all in the same Postgres instance.

The Bad

The licence is split, and the Docker tag you pick decides what you get. TimescaleDB is dual-licensed. Everything outside the tsl/ directory is Apache 2.0. Everything inside it falls under the Timescale License, which is source-available and not OSI-approved. The columnstore, continuous aggregates, and the job scheduler all live on the TSL side.

That distinction has teeth. timescale/timescaledb:latest-pg17 ships both halves. timescale/timescaledb:latest-pg17-oss ships the Apache-only build, where hypertables work fine and both of the following fail:

Terminal window
ERROR: functionality not supported under the current "apache" license.
HINT: To access all features and the best time-series experience, try out Timescale Cloud.

That is the real error from the -oss image, thrown by both enable_columnstore and CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous). If your policy requires OSI licences, you get hypertables and nothing else, which means no compression and no downsampling. Decide that before you architect around continuous aggregates.

Ingestion throughput is lower than InfluxDB or VictoriaMetrics. You are inserting rows into a Postgres table, so you pay WAL writes, tuple overhead, and index maintenance on every batch. Batched COPY into a hypertable gets you a long way, and for a home lab or a few hundred hosts it is a non-issue. At real scale you will be tuning shared_buffers and checkpoints, adding read replicas, and eventually asking whether a purpose-built engine would have been less work.

Operational overhead. TimescaleDB still has to play by Postgres rules: WAL, checkpoint tuning, vacuum, background workers. It’s not a set-it-and-forget-it appliance.

Continuous aggregates add complexity. TimescaleDB’s answer to downsampling is continuous aggregates, which are powerful but require upfront thinking about what you want to pre-compute. They are also TSL-only, per the licence note above.

A Typical Docker Compose Setup

docker-compose.yml
services:
timescaledb:
# Drop the -oss suffix off this tag and you lose compression
# and continuous aggregates. Add it back only on purpose.
image: timescale/timescaledb:2.29.2-pg17
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: changeme
POSTGRES_DB: metrics
volumes:
- timescaledb_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
volumes:
timescaledb_data:

Ingesting with Python:

write_timescale.py
import psycopg2
from psycopg2.extras import execute_values
from datetime import datetime, timezone
conn = psycopg2.connect(
host="localhost",
database="metrics",
user="postgres",
password="changeme"
)
# Create hypertable (one-time setup)
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS metrics (
time TIMESTAMPTZ NOT NULL,
host TEXT NOT NULL,
region TEXT NOT NULL,
cpu_percent FLOAT8
);
-- by_range() is the current dimension-builder form. The old
-- create_hypertable('metrics', 'time') signature still works
-- but is deprecated.
SELECT create_hypertable('metrics', by_range('time'), if_not_exists => TRUE);
""")
conn.commit()
# Insert data
with conn.cursor() as cur:
data = [
(datetime.now(timezone.utc), 'server-01', 'us-west', 45.2),
]
execute_values(
cur,
"INSERT INTO metrics (time, host, region, cpu_percent) VALUES %s",
data
)
conn.commit()
conn.close()

VictoriaMetrics: Built for Prometheus

VictoriaMetrics is ruthlessly pragmatic. It was designed to solve one problem well: be a long-term storage backend for Prometheus. Everything else cascades from that.

The Good

Cardinality handling is the headline feature. VictoriaMetrics shrugs at high cardinality and has no hard limit. If you’re tagging everything with request IDs, customer UUIDs, and trace IDs, it will keep going where InfluxDB 2.x falls over. This is its secret weapon.

Compression is efficient, and the licence is Apache 2.0 end to end. No TSL directory, no feature gated behind an edition. On the compression numbers: VictoriaMetrics claims 7x less storage than Prometheus, 10x less RAM than InfluxDB, and 70x more data points than TimescaleDB. Those figures all trace back to the project’s own 2019 benchmark posts, which means they are vendor claims about vendor-chosen workloads. Treat them as directional. What is not in dispute is that VictoriaMetrics keeps winning long-term-storage bake-offs on disk cost for Prometheus-shaped data.

Query language is MetricsQL, which is essentially Prometheus PromQL plus some extensions. If your team knows Prometheus, they already know how to query VictoriaMetrics. The learning curve is basically zero.

Resource footprint is tiny. One static binary, no external dependencies, no JVM, no separate index service. The deployment shape is the part you can verify in an afternoon: one process, one data directory, one flag for retention.

Retention is configurable and automatic. Set -retentionPeriod and old data is deleted on its own. Watch the units, because this trips up everyone: an unsuffixed value is counted in months. -retentionPeriod=12 means one year, not twelve days. The default is 1M and the minimum is 24h. Suffixes s, h, d, w, M, y all work, so write -retentionPeriod=1y and remove the ambiguity.

The Bad

It only does metrics. VictoriaMetrics stores time series and nothing else. Anything else needs a second system.

Querying is metrics-first. If you want complex joins or relational queries, TimescaleDB or InfluxDB 3’s SQL is cleaner. VictoriaMetrics excels at “sum this metric over time” and “show me the rate of change,” not SQL JOINs.

Community is smaller. InfluxDB has a massive ecosystem. VictoriaMetrics is growing fast but doesn’t have the same breadth of integrations (yet).

Fresh samples are invisible for 30 seconds. The -search.latencyOffset flag defaults to 30s, which means a point you just wrote will not appear in /api/v1/query results until that window passes. This is deliberate, so partial scrape results don’t produce wrong last data points, and it is completely baffling the first time you write one sample by hand and get an empty query back. Lower it or pass latency_offset per query if you need a faster read-back loop.

A Typical Docker Compose Setup

docker-compose.yml
services:
victoriametrics:
image: victoriametrics/victoria-metrics:v1.151.0
ports:
- "8428:8428"
command:
- "-storageDataPath=/data"
# Unsuffixed means MONTHS. This is one year.
- "-retentionPeriod=12"
volumes:
- victoriametrics_data:/data
healthcheck:
# The image has no curl, and busybox wget resolves "localhost"
# to [::1] while VictoriaMetrics listens on IPv4 only. Both
# mistakes leave the container permanently unhealthy.
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8428/-/healthy"]
interval: 10s
timeout: 5s
retries: 5
volumes:
victoriametrics_data:

That healthcheck comment is the single most useful thing in this article, so let me be explicit about it. The almost-universally-copied ["CMD", "curl", "-f", "http://localhost:8428/-/healthy"] fails twice over on this image. There is no curl binary. And swapping in busybox wget is not enough either, because busybox resolves localhost to the IPv6 loopback and gets wget: can't connect to remote host: Connection refused. Use the literal 127.0.0.1 and the container reports healthy in about 30 seconds.

Ingesting with Python:

write_vm.py
import requests
VM = "http://localhost:8428"
def write_metric(name, labels, value, timestamp_ms=None):
"""Push one sample in Prometheus text exposition format."""
label_str = ",".join(f'{k}="{v}"' for k, v in labels.items())
line = f"{name}{{{label_str}}} {value}"
if timestamp_ms is not None:
line += f" {timestamp_ms}"
resp = requests.post(
f"{VM}/api/v1/import/prometheus",
data=line.encode(),
timeout=10,
)
resp.raise_for_status()
write_metric("cpu_usage", {"host": "server-01", "region": "us-west"}, 45.2)

That is a plain HTTP POST of Prometheus text format straight at port 8428. No push gateway, no protobuf, no client library. If you want actual Prometheus remote write, that is Prometheus’ job: point its remote_write block at http://victoriametrics:8428/api/v1/write and let it handle the snappy-compressed protobuf. Do not hand-roll that from Python.

Ingestion Throughput: Why There Is No Table Here

Every other comparison on this topic hands you a points-per-second table. This one does not, because I cannot reproduce any of the tables in circulation and neither can you.

Trace those numbers back and they come from three places, all of them a vendor benchmarking its own product: InfluxData’s TSBS runs, TigerData’s rebuttal posts, and VictoriaMetrics’ 2019 blog series. Each one chose the batch size, cardinality, hardware, and query mix that flattered its own engine. Nobody is lying. All of them are useless for predicting your workload, because throughput on all three engines swings by more than an order of magnitude depending on:

The directional ranking is stable, and that is the part worth keeping. For Prometheus-shaped metric data at the same hardware budget, VictoriaMetrics ingests the most per core, InfluxDB 2.x is competitive right up until cardinality climbs, and TimescaleDB trades raw throughput for SQL and for living inside the database you already run.

If you need a real number, run TSBS against all three with your schema and your batch size. It is an afternoon of work and it beats every table on the internet, including the one I just refused to write.

Which One Should You Actually Pick?

InfluxDB if:

TimescaleDB if:

VictoriaMetrics if:

The Real Talk

Most teams pick InfluxDB first because it’s the household name. Then, six months into production, they either hit 2.x cardinality walls (at 2 AM, naturally) or open a Cloud invoice and discover the storage line item.

TimescaleDB quietly wins in shops that are already Postgres-native. No new operational knowledge, same monitoring stack, same query patterns. It’s boring in the best way, right up until someone in legal asks what the Timescale License is.

And VictoriaMetrics? It’s the scrappy option that works a little too well once you understand what it’s for. It won’t replace your application database, and it won’t handle ad-hoc analytics. But for metrics? It’s a laser-focused tool that does one thing and does it obsessively well.

Pick one, set it up, monitor it with the other two, and call it even.

Common Questions

Is TimescaleDB open source?

Partly. Code outside the tsl/ directory is Apache 2.0, and code inside it uses the Timescale License, which is source-available and not OSI-approved. Compression, continuous aggregates, and the job scheduler are all TSL. The -oss Docker tags give you an Apache-only build with hypertables and none of those features.

Should I still use Flux for new InfluxDB work?

No. InfluxData put Flux into maintenance mode, recommends InfluxQL for 2.x, and did not carry Flux into InfluxDB 3. The one exception is scheduled tasks in 2.x, since Flux is the only task language 2.x has. Write dashboards and ad-hoc queries in InfluxQL, and keep Flux inside tasks.

Why does my VictoriaMetrics container show as unhealthy?

The VictoriaMetrics image ships no curl, so any healthcheck copied from a curl-based example fails immediately. Busybox wget also fails against localhost, because it resolves to IPv6 while VictoriaMetrics listens on IPv4 only. Use wget -q -O /dev/null http://127.0.0.1:8428/-/healthy instead.

Can I run any of these three on a Raspberry Pi?

VictoriaMetrics yes, comfortably, on a Pi 4 or newer with an SSD. TimescaleDB works for a home lab but wants more RAM than a Pi likes once compression jobs run. InfluxDB 2.x is the worst fit, because its index lives in memory and a Pi runs out first. Avoid SD cards for all three.

How much does InfluxDB Cloud actually cost to store metrics?

Storage on the Usage-Based Plan is $0.002 per GB-hour as of September 2026, roughly $1.46 per GB-month. That is about $290 a month for 200GB, before write, query, or egress charges. Self-hosting the same 200GB on a VPS with a block volume costs a small fraction of that.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
PgBouncer for Connection Pooling
Next Post
SQLite Replication: Litestream and rqlite

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts