Back to all posts

TimescaleDB vs ClickHouse: Which Should You Choose?

TimescaleDB wins on operational simplicity, joins, and ACID transactions. ClickHouse wins on aggregation speed and storage compression. An on-call-focused comparison.

Temps Team

Temps Team

May 17, 2026 · 2mo ago

TimescaleDB vs ClickHouse: Which Should You Choose in 2026?

TimescaleDB beats ClickHouse on operational simplicity, joins, and ACID transactions — ClickHouse beats TimescaleDB on aggregation speed (10–100×) and storage compression (5–10×). For most teams under 50M events/month, TimescaleDB on a single VPS is the right call. Above that threshold, you'll want ClickHouse for analytical reads — but Postgres stays the system of record.

DimensionTimescaleDBClickHouse
Best forOLTP-shaped reads, joins, mutable dataAggregations, funnels, high-volume analytics
Day-1 setupCREATE EXTENSION timescaledb; on any Postgres 13+Multi-file config, ZooKeeper/Keeper for HA
Aggregation speedBaseline10–100× faster for columnar scans
Storage compressionRow-store (B-tree)5–10× smaller (columnar + ZSTD)
JoinsNative Postgres joins, 30 years of planner maturityRequires Dictionary workaround at scale
Mutable data (UPDATE/DELETE)Sync, transactional, routineAsync "mutations" — not transactional, heavy
Schema changesALTER TABLE online, CREATE INDEX CONCURRENTLYORDER BY key changes require table-swap dance
On-call familiarityEvery Postgres skill transfersKeeper quorum failures, OOM query tuning
Backup / PITRpg_dump, WAL-G, mature PITRThree options, each with tradeoffs; PITR harder
Self-hosting costOne Postgres processKeeper/ZooKeeper + CH server per replica

You're three months into running an analytics product. The graph that worked fine at 50M events now takes 11 seconds to load. Someone in #engineering says "have you considered ClickHouse?" — like it's a free upgrade. It is not a free upgrade. It is a different operating system.

I'm going to walk through what actually changes when you switch, the way someone whose pager has gone off at 3am for both systems would walk through it.

TL;DR: TimescaleDB is Postgres with a time-series superpower; ClickHouse is a columnar OLAP database that happens to speak SQL. TimescaleDB wins on operational simplicity, joins, transactions, and "you already know how to run it." ClickHouse wins on aggregation speed (10–100×) and storage compression (5–10×). The honest answer for most teams is "both, hybrid" — but the cost of running both is a real line item.


The mental model

Before features, ops, or backups, the most useful framing is what these databases actually are.

TimescaleDB is PostgreSQL with one extension installed. Every Postgres skill you have transfers: psql, pg_dump, pg_basebackup, WAL-G, logical replication, every ORM you've used, every EXPLAIN ANALYZE muscle. The hypertable abstraction (a Postgres table chunked by time, with each chunk being a regular table) means your data is still rows in a B-tree, just with smarter partitioning.

ClickHouse is a columnar OLAP store. It speaks a SQL-shaped dialect, but the storage engine, the query planner, the replication model, the failure modes, and the operational surface are all unrelated to anything in the Postgres family. Mutations are async. There are no transactions. The default MergeTree doesn't dedupe. JOIN is not where the database wants to spend its time.

If you switch from one to the other thinking "it's just a database swap," your first 90 days will be educational.

Functionality: what the queries actually look like

Time bucketing and gap-fill

TimescaleDB:

SELECT
  time_bucket_gapfill('5 minutes', timestamp) AS bucket,
  COUNT(*) AS events,
  LOCF(COUNT(*)) AS events_filled
FROM events
WHERE project_id = 7
  AND timestamp BETWEEN now() - interval '24 hours' AND now()
GROUP BY 1
ORDER BY 1;

The time_bucket_gapfill requires both a strict WHERE time clause and is fussy about column ordering at GROUP BY time. We have a footnote in our internal docs about not casting time_bucket() in the same query level as GROUP BY because the planner picks a worse plan. It works, it's just ceremonious.

ClickHouse:

SELECT
  toStartOfInterval(timestamp, INTERVAL 5 MINUTE) AS bucket,
  count() AS events
FROM events
WHERE project_id = 7
  AND timestamp BETWEEN now() - INTERVAL 24 HOUR AND now()
GROUP BY bucket
ORDER BY bucket
WITH FILL STEP INTERVAL 5 MINUTE;

The WITH FILL clause is just an annotation on ORDER BY. Less ceremony, less to memorize.

Verdict: ClickHouse, decisively. Anyone who's debugged a misbehaving time_bucket_gapfill knows.

Funnels

TimescaleDB:

WITH step1 AS (
  SELECT visitor_id, MIN(timestamp) AS t1
  FROM events WHERE event_name = 'signup' AND ... GROUP BY visitor_id
),
step2 AS (
  SELECT s1.visitor_id, MIN(e.timestamp) AS t2
  FROM step1 s1 JOIN events e ON e.visitor_id = s1.visitor_id
  WHERE e.event_name = 'first_action' AND e.timestamp > s1.t1 AND ...
  GROUP BY s1.visitor_id
)
SELECT
  COUNT(*) AS reached_step1,
  COUNT(t2) AS reached_step2,
  COUNT(t2)::float / COUNT(*) AS conversion_rate
FROM step1 LEFT JOIN step2 USING (visitor_id);

ClickHouse:

SELECT
  level,
  count() AS visitors
FROM (
  SELECT visitor_id,
    windowFunnel(86400)(timestamp,
      event_name = 'signup',
      event_name = 'first_action'
    ) AS level
  FROM events WHERE ... GROUP BY visitor_id
)
GROUP BY level ORDER BY level;

windowFunnel(seconds)(timestamp, cond1, cond2, ..., condN) is a single aggregate. You pass it conditions and a time window, it returns "how many steps did this user complete." Adding a fourth funnel step is one more line. In Timescale you write another CTE.

Verdict: ClickHouse, again decisively. Funnel queries are where columnar OLAP earns its keep.

Joins to relational data

You have an events table and you want to label each row with the project name from projects.

TimescaleDB: JOIN projects p ON p.id = e.project_id. Done. Postgres planners are 30 years old; this Just Works.

ClickHouse: also possible, but it wants you to use a Dictionary (an in-memory hash table sourced from the OLTP database, refreshed periodically) or denormalize the project name into the events row at write time. A naive JOIN works for small dimension tables but is the #1 source of "ClickHouse is slow" tickets when people scale up.

Verdict: TimescaleDB, decisively. If your queries cross multiple business entities, you're going to fight ClickHouse here.

Updates and deletes

A user emails support: "delete my data." A project is renamed and you want to update 8M rows. A bug caused last week's events to have a wrong event_name and you need to fix them.

TimescaleDB: UPDATE and DELETE are sync, transactional, and routine. The thing you don't want to do is an unindexed UPDATE over 100M rows during peak traffic, but the operation itself is normal.

ClickHouse: ALTER TABLE ... UPDATE WHERE ... is async, eventually consistent, heavy, and not transactional. The docs literally call them "mutations" to signal that they're a different concept. Doing more than a handful per day is unusual; doing them on a partitioned hot table can starve regular merges.

Verdict: TimescaleDB, by a mile. If your data is mutable, ClickHouse will make you uncomfortable.

Approximate uniques and top-K

TimescaleDB: COUNT(DISTINCT user_id) works. The hyperloglog extension exists. It's fine.

ClickHouse: uniq(), uniqHLL12(), uniqCombined64(), topK(10)(event_name), quantilesTDigest(0.5, 0.95, 0.99)(latency). All first-class, all fast, all on by default.

Verdict: ClickHouse. Not a contest.

Operations: what running it actually feels like

Day-1 setup

TimescaleDB: CREATE EXTENSION timescaledb; against any Postgres 13+ install. You're done. Use the same monitoring you use for Postgres. Use the same backup tool you use for Postgres. The Hetzner box you already have is fine.

ClickHouse: install clickhouse-server, configure users.xml, configure config.xml, decide whether you want a single node (fine for dev, terrifying for prod), a replicated cluster (needs ZooKeeper or Keeper), or sharded+replicated (needs Keeper and a routing layer like CHProxy or clickhouse-balancer). The Helm chart exists. It's not bad. It's also not "five minutes."

Verdict: TimescaleDB. The operational on-ramp is a runway compared to ClickHouse's cliff.

Replication and HA

TimescaleDB: streaming replication with pg_basebackup + WAL shipping, or logical replication with pglogical. Failover is the same as Postgres failover (pg_auto_failover, Patroni, Stolon). On-call already knows this.

ClickHouse: replication is built into the ReplicatedMergeTree engine, coordinated via Keeper (or ZooKeeper). It's actually quite elegant — every replica pulls parts independently and verifies checksums — but Keeper is now the most fragile thing in your stack. When Keeper has a hiccup, replicas stop accepting writes. Most ClickHouse outages I've seen were Keeper-related, not ClickHouse-related.

Verdict: TimescaleDB by familiarity, ClickHouse by elegance once you've paid the learning tax. If you've never run ZooKeeper-style coordinators, budget two weeks.

Schema changes

TimescaleDB: ALTER TABLE ADD COLUMN is fast on hypertables (no full rewrite). Indexes can be added concurrently. You've done this before.

ClickHouse: most ALTER operations are async and rewrite parts in the background. Adding a column is fast; backfilling its values is a mutation, which is a different beast. Renaming columns is fine. Changing a ORDER BY key is impossible — you create a new table, INSERT INTO new SELECT FROM old, swap names. People build whole runbooks around this.

Verdict: TimescaleDB, especially as the schema evolves frequently early in product life.

Memory tuning

TimescaleDB: default Postgres knobs. shared_buffers, work_mem, effective_cache_size. Tuning advice is plentiful.

ClickHouse: max_memory_usage, max_memory_usage_for_user, max_bytes_before_external_group_by, max_bytes_before_external_sort. Default settings are aggressive — a single bad query can OOM the whole server. Production deployments end up with per-user quota profiles, query complexity limits, and a SETTINGS clause appended to half their dashboard queries.

Verdict: TimescaleDB by familiarity. ClickHouse rewards the time investment but punishes the first month.

Observability

TimescaleDB: pg_stat_statements, every Postgres dashboard you've ever seen, datadog/grafana integrations that go back a decade. EXPLAIN ANALYZE reads like English.

ClickHouse: system.query_log is genuinely fantastic — every query, its timing, memory, rows read, replica used, all in a queryable table. The downside is that you're querying ClickHouse to debug ClickHouse. EXPLAIN PIPELINE is great when it makes sense; the planner output is dense.

Verdict: Tied. ClickHouse's introspection is technically richer; Postgres's tooling ecosystem is older and broader.

Backups and recovery: what happens at 3am

This is where the conversation gets pointed.

TimescaleDB

pg_dump, pg_basebackup, WAL-G — same as any Postgres. Point-in-time recovery is mature; we use it in production. A 50GB hypertable dumps in roughly 8 minutes. The restore is well-understood.

The one trap: TimescaleDB chunks need timescaledb_pre_restore() and timescaledb_post_restore() wrappers, otherwise you'll orphan chunks that break CREATE INDEX on the hypertables. We have a footnote about that in our own runbook because it bit us once.

ClickHouse

Three options, each with different tradeoffs:

  1. BACKUP DATABASE temps_analytics TO S3('https://bucket', 'access_key', 'secret_key') — server-side, fast, ClickHouse 22.8+. Requires the CH server to have S3 credentials, which is not always operationally clean.
  2. clickhouse-backup (Altinity tool) — sidecar process, doesn't need server-side credentials, integrates with cron and tier-1 cloud storage providers. The de-facto choice for self-hosted production.
  3. Disk backups — copy the data directory while the server is stopped. Simple and reliable, but downtime-required.

PITR exists for ClickHouse but is genuinely harder than for Postgres. There's no WAL equivalent; you back up parts and replay log pieces. If your RPO is "5 minutes ago," you'll spend more time on this than you expected.

Verdict: TimescaleDB, especially if you already have WAL-G. ClickHouse backups are fine — they're just additional surface area on top of the analytics surface area.

Storage and cost

A real example from a customer install we migrated:

  • Timescale hypertable, 4 months of events, ~180M rows: 64 GB on disk
  • Same data in ClickHouse MergeTree with ZSTD(3) compression on string columns: 7.8 GB on disk

That's 8× compression. On a Hetzner CX31 (160 GB SSD), that's the difference between "this fits on one box" and "we need to start thinking about archival."

Verdict: ClickHouse, decisively. Columnar storage with LowCardinality(String) and ZSTD is in a different league.

The on-call burden

Honest accounting of the things I've personally been paged for, by category:

TimescaleDB:

  • Slow queries from a missing index
  • Connection pool exhaustion
  • WAL bloat from a stuck logical replication slot
  • An UPDATE that locked the wrong rows
  • Disk full because chunk retention was misconfigured

ClickHouse:

  • Keeper quorum lost (twice, both in the same week)
  • A query that OOM'd the server because max_memory_usage defaulted too high
  • A ReplicatedMergeTree insert that got stuck because the replica's queue was full
  • A schema change that required the new-table-rename dance and we forgot to drop the old one for two weeks (paying double storage)
  • A user pasted a SELECT * against the events table from the BI tool and brought the cluster to its knees

The categories are different. ClickHouse incidents tend to be more dramatic but less frequent once you've stabilized. Postgres incidents are more frequent but more boring.

Maintainability over 12 months

The single biggest thing nobody tells you: two systems means double the on-call training, double the runbooks, double the dashboards, double the upgrade cycles. If you're a 5-person team, the second database is a real cost.

TimescaleDB-only at 12 months: queries get slower as data grows, but the operational team didn't grow.

ClickHouse-only at 12 months: queries are fast, but you've spent ~30% of your DBA time on coordination layer issues, query memory tuning, and integrating CH with a stack designed for Postgres.

Hybrid (PG for OLTP, CH for analytics) at 12 months: best query performance, but you've added a fan-out layer (write to PG, replicate to CH), a parity test harness (so the two answer the same question identically), and a runbook for "what happens when CH is behind."

There is no free choice here.

So when do you actually switch?

A rule of thumb after watching a few teams make this call:

  • < 50M events/month, < 100GB hot data: TimescaleDB. The Postgres familiarity is worth more than the query speed.
  • 50M–500M events/month: Honest evaluation. If your queries are heavily aggregational (funnels, retention, large rollups), the move is worth it. If they're OLTP-shaped (recent activity, joined to user/project metadata), stay.
  • > 500M events/month: You are going to ClickHouse eventually. The only question is whether you go fully or hybrid.
  • You need column-level retention or per-tenant TTL: ClickHouse's partition+TTL story is cleaner than Timescale's data-retention policies.
  • You have one DBA and they've never run ZooKeeper: Don't switch yet. Hire first or pay for ClickHouse Cloud / Altinity.

What we ended up doing for Temps

Verified Temps claims for this architecture:

  1. Temps ships analytics on PostgreSQL + TimescaleDB by default — the single-binary default install requires no ClickHouse setup.
  2. Above ~50M events/month, operators can enable an optional ClickHouse backend via four environment variables (TEMPS_CLICKHOUSE_URL, TEMPS_CLICKHOUSE_DATABASE, TEMPS_CLICKHOUSE_USER, TEMPS_CLICKHOUSE_PASSWORD) — no rebuild, no cargo feature flag needed.
  3. The same aggregation query that takes ~8 seconds on TimescaleDB runs in ~80ms on ClickHouse — verified in ADR-012 against the Temps events schema.

Temps ships analytics on PostgreSQL + TimescaleDB by default, because the indie-hacker default install is a single binary on a single Hetzner box, and "install ZooKeeper" is not a sentence that belongs in our quickstart.

For operators who outgrow that, Temps supports an optional ClickHouse backend (ADR-012). The hybrid model: Postgres remains the system of record, ClickHouse is a derived columnar replica that the operator brings their own (Altinity, ClickHouse Cloud, or self-hosted). Postgres handles transactional reads and joins; ClickHouse handles funnel queries, retention cohorts, and dashboards over millions of rows. A fan-out worker (ChFanoutWorker) streams new events from the Postgres outbox to ClickHouse via ReplacingMergeTree(_version) — so retries are safe and dedup is automatic.

Enabling the ClickHouse backend takes four environment variables and a restart — same binary, no code change:

export TEMPS_CLICKHOUSE_URL=https://ch.example.internal:8443
export TEMPS_CLICKHOUSE_DATABASE=temps_analytics
export TEMPS_CLICKHOUSE_USER=temps
export TEMPS_CLICKHOUSE_PASSWORD=...

./temps serve

Temps requires ClickHouse 24.3 or newer. ClickHouse Cloud, Altinity, and self-hosted deployments all work — Temps treats CH as an opaque HTTP target.

We picked this shape because it preserves the "single binary works" promise for small installs while opening a real upgrade path. We didn't try to make ClickHouse a managed service you spin up like Postgres, because anyone running enough analytics to need ClickHouse is also running their own coordination layer, and bundling that into a PaaS would either be shallow or huge.

Temps is Apache 2.0 and free to self-host — you only pay for the server you already run it on. Temps Cloud, a managed add-on for telemetry retention, offsite backups, and AI credits, is coming soon and isn't priced here yet.

The decision document is in our repo if you're interested in the full reasoning. The short version: the answer to "TimescaleDB or ClickHouse" for an analytics workload at scale is usually "both," and that has costs you should price honestly before you start.


Frequently Asked Questions

Should I use TimescaleDB or ClickHouse for time-series data?

It depends on your query shape. TimescaleDB is the right default for most teams: it runs on any Postgres install, every existing skill transfers, and it handles millions of events per month comfortably. ClickHouse wins when your queries are heavily aggregational (funnels, retention cohorts, large rollups) and your data volume exceeds ~50M events/month. For analytics platforms, a hybrid approach — Postgres as system of record, ClickHouse as derived replica — gives both without sacrificing ACID guarantees.

How much faster is ClickHouse than TimescaleDB?

For columnar aggregations (funnels, GROUP BY, top-K), ClickHouse is typically 10–100× faster than TimescaleDB. The same funnel query that takes ~8 seconds in TimescaleDB runs in ~80ms in ClickHouse for the same dataset. For join-heavy OLTP queries (recent activity cross-referenced with user/project metadata), TimescaleDB is faster — ClickHouse's JOIN performance degrades without Dictionary pre-loading.

Can I run both TimescaleDB and ClickHouse at the same time?

Yes — this is the hybrid model. Postgres/TimescaleDB handles writes and relational reads; ClickHouse gets a fanout of every analytics event and handles aggregation queries. The tradeoff is operational complexity: two systems means two upgrade cycles, two on-call runbooks, and a parity test harness to keep them answering the same questions identically.

Does TimescaleDB support ClickHouse-style funnel queries?

TimescaleDB requires multi-step CTEs for funnel analysis. ClickHouse has a first-class windowFunnel(seconds)(timestamp, cond1, cond2, ...) aggregate that expresses the same logic in a single query. For teams doing heavy funnel work at scale, this is one of the most practical reasons to adopt ClickHouse.

What is the storage difference between TimescaleDB and ClickHouse?

ClickHouse's columnar MergeTree with ZSTD(3) compression is typically 5–10× smaller than a row-store Postgres/TimescaleDB table. In practice, 64 GB of event data in TimescaleDB can compress to 7–8 GB in ClickHouse — an 8× reduction. This directly affects how long a single box remains viable.

Does Temps support ClickHouse?

Yes. Temps ships with TimescaleDB as the default analytics backend. For high-volume installs (above ~50M events/month), operators can enable an optional ClickHouse backend by setting four environment variables — no rebuild required. Temps treats CH as a bring-your-own analytical replica: Postgres stays the system of record, and a background fan-out worker replicates events to ClickHouse asynchronously. See the how-to guide for setup details.


If you're building analytics on Postgres today and the queries are getting slow, Temps gives you the hybrid path without you having to design the fan-out layer yourself. Temps is Apache 2.0 and free to self-host. If you've already gone all-in on ClickHouse and are happy, send us a war story — we collect them.

#timescaledb#clickhouse#postgresql#observability#database comparison