Why We Chose TimescaleDB for Pulse
The technical decision behind using TimescaleDB + PostgreSQL for time-series metrics in Pulse — and why we said no to a dedicated TSDB.
Servelium Engineering
公開日
When we started building Pulse, the storage question came first: metrics arrive every few seconds from every monitored host, nearly all queries are "last N hours for host X," and old data matters less every day. Classic time-series workload. The obvious answers were InfluxDB, ClickHouse, VictoriaMetrics — or just PostgreSQL with TimescaleDB.
We picked TimescaleDB. Here's the actual reasoning, not the marketing version.
The shape of our data
A Pulse agent ships a metrics envelope every 5 seconds:
{
"host_id": "srv-9f2a",
"ts": 1714298400,
"cpu": { "usage": 23.4, "load1": 0.8 },
"mem": { "used_mb": 3072, "total_mb": 8192 },
"disk": [{ "mount": "/", "used_pct": 64.2 }],
"net": { "rx_kbps": 1240, "tx_kbps": 380 }
}
At 10,000 hosts that's ~120M rows/day. Significant, but not "build a custom storage engine" significant.
Why not a dedicated TSDB?
Three reasons, in order of weight:
1. We already need PostgreSQL. Accounts, hosts, alert rules, status pages, billing — all relational. A separate TSDB means two databases to operate, back up, and keep consistent. For a small team, every extra stateful system is a tax on everything else you do.
2. Our queries are SQL-shaped. Alert evaluation joins metrics against per-customer rules; status pages join metrics against host metadata and incident history. In TimescaleDB that's one query with a JOIN. With a separate TSDB it's two round-trips and a merge in application code.
3. Operational maturity. PostgreSQL's backup, replication, and failure modes are documented to death. When something breaks at 3 AM, pgBackRest and twenty years of Stack Overflow beat a niche database's GitHub issues.
What TimescaleDB actually gives us
A hypertable partitions data by time under the hood while looking like a normal table:
CREATE TABLE metrics (
ts TIMESTAMPTZ NOT NULL,
host_id UUID NOT NULL,
cpu_usage REAL,
mem_used INTEGER,
rx_kbps INTEGER,
tx_kbps INTEGER
);
SELECT create_hypertable('metrics', 'ts', chunk_time_interval => INTERVAL '1 day');
Compression turned out to be the headline feature. Metrics older than a day compress ~94% in our workload:
ALTER TABLE metrics SET (timescaledb.compress,
timescaledb.compress_segmentby = 'host_id');
SELECT add_compression_policy('metrics', INTERVAL '24 hours');
Continuous aggregates precompute the rollups that dashboards read, so a 30-day chart never scans raw 5-second data:
CREATE MATERIALIZED VIEW metrics_5m
WITH (timescaledb.continuous) AS
SELECT time_bucket('5 minutes', ts) AS bucket,
host_id,
avg(cpu_usage) AS cpu_avg,
max(cpu_usage) AS cpu_max
FROM metrics
GROUP BY bucket, host_id;
And retention policies delete old chunks by dropping files instead of running DELETE:
SELECT add_retention_policy('metrics', INTERVAL '13 months');
The honest trade-offs
- At Datadog scale (billions of active series), a columnar TSDB wins. We are not Datadog, and neither are our customers.
- Very high cardinality (per-process metrics on thousands of hosts) needs schema care — we aggregate at the agent before shipping.
- You manage PostgreSQL tuning yourself. We were going to anyway.
The principle underneath
Choose the database that makes your whole system simpler, not the one that wins the benchmark for one table. One PostgreSQL with TimescaleDB means one backup strategy, one connection pool, one thing to monitor — which, for a monitoring product, is a pleasing kind of recursion.