Our ClickHouse setup to reduce cost
ObsessionDB lowers ClickHouse cost by cutting how much you use, not what you pay per unit. We keep one copy of the data on object storage instead of one per replica, size compute to the workload instead of duplicating it for availability, move about four times less data between nodes during merges, and charge nothing for data transfer. Storage is billed per compressed terabyte, so your sort order and column types decide that line.
$25,600 a month against well north of $150,000. Those are the two infrastructure bills behind our 10-billion-row benchmark, twenty nodes on each side. The migration off BigQuery we published in June went the same way: about $8,000 a month in scan charges became a three-node cluster for around $2,000. We have already covered latency and scale. This post covers cost: what a ClickHouse bill is made of, which part of our setup cuts each line, and what you can change on any cluster.
What a ClickHouse bill is made of
A ClickHouse bill can have four lines. Compute, because nodes need RAM and cores for queries and for the merges that run between them. Compressed storage, because MergeTree compresses each column on write, and those bytes are what gets metered. Data transfer, because clusters move parts between nodes and results out to clients, and cloud providers charge for bytes leaving a region. Per-query charges only appear on warehouses like BigQuery and Snowflake. ClickHouse bills for the capacity you provision, and queries on it cost nothing extra.
The lines you see depend on who runs the cluster. Self-hosted, you pay your cloud provider for instances, for storage times the number of replicas, and for egress, plus the engineers on call. ClickHouse Cloud meters compute per minute, compressed storage per terabyte and egress per gigabyte. Bring-your-own-cloud services add a management fee per vCPU on top of your provider's bill. ObsessionDB charges a flat monthly price per node and a price per compressed terabyte, with no transfer or per-query charges. Our pricing:
| Term | ObsessionDB |
|---|---|
| Compute, one 64 GB node for a month | $1,280 (8 units of 2 vCPU and 8 GB at $160) |
| Storage, per compressed TB per month | $25 |
| Data transfer out | Free, both directions |
| Per query | None |
The chart at the top applies these prices to three 64 GB nodes at 7x compression, from 10 TB up to a petabyte. The same workload needs fewer of those nodes and fewer of those terabytes on our setup, for the reasons below.
The setup, layer by layer
Each part of the setup below reduces how much of something you pay for: copies of the data, nodes, bandwidth, compressed terabytes, or data leaving the region. The prices stay the same.
One copy of the data
Three replicas means three copies. With ReplicatedMergeTree, one server writes a part and every other replica downloads it to its own disk, so three replicas of a 54 GiB table hold about 162 GiB. At S3 Standard's $23.60 per terabyte, 10 TB on three replicas is 30 TB and $708 a month. The same 10 TB on ObsessionDB is $250. Putting MergeTree on an S3 disk does not fix this, because each replica still keeps its own objects and metadata. Zero-copy replication would let them share, but it has been off by default since 22.8 and is not recommended for production.
Alloy, our engine built against the SharedMergeTree API, stores one copy of the data on object storage and nothing permanent on the nodes. Adding a node moves no data, and removing one leaves it untouched. You pay for storage once, however many nodes you run.
Compute sized to the job
On a replicated cluster, each replica is insurance you pay full price for, all the time. It has to be as big as the primary in case it takes over, and it runs the same merges on its own copy while it waits. On shared storage, a failed node is replaced by a new one attaching to the same data, so nothing sits on standby.
We run a customer's serving nodes as one group and their backfills and benchmarks as another, both reading the same data. The serving nodes are sized for serving, not for the biggest job of the quarter. Moving a node between groups copies no data, and when a job is done the extra node goes away, along with its cost. Our sizing rule from the scaling post: fewer, bigger nodes, and at least three. Big merges and joins need one node's memory, and a two-node cluster loses half its capacity during a rolling update.
Bandwidth, the hidden cost
Network-optimized AWS instances cost about a third more for the same CPU and memory. A c6in.xlarge costs $0.2268 an hour against $0.17 for a c6i.xlarge, both with 4 vCPU and 8 GiB, and the m6in.xlarge costs 45% more than the m6i.xlarge (us-east-1, September 22, 2026). No ClickHouse price list has a bandwidth line. You pay for it in the size of the nodes you need.
A distributed ClickHouse cluster spends most of its time moving data, so bandwidth limits latency and concurrency before CPU does. One of our six-node clusters moved about 400 TB between nodes in a single week of heavy ingest. When six nodes place merges at random, only 17% of the data a merge reads is local. Ours are around 70% local, roughly four times less merge traffic, and parallel-replica reads use the same placement to stay at least half local.
The distributed cache does the rest. Every node's NVMe is part of one shared cache, parts are cached as soon as they are written, and a fetch from another node takes under a millisecond, where object storage takes tens of milliseconds. Faster requests let each node serve more on the same bandwidth and send fewer GET requests to object storage, which bills them per thousand. The cache runs on disks you already pay for as part of the node. The same speed from storage would mean S3 Express One Zone at $0.11 per GB-month, 4.8x the price of S3 Standard even after the 2025 price cut. The result is smaller nodes for the same load, or more load on the same nodes, and that is where the benchmark gap at the top comes from.
Compression
Storage is billed per compressed terabyte, so your compression ratio sets the storage bill. ClickHouse's docs say around 10x for typical analytical data, and our clusters are in that range: 7.94x on a blockchain transfers partition, about 6.5x across one cluster (130 TB of raw data stored in 20 TB), 16x on our logs and 13x on our metrics, and 3.05x on the 56 TB table full of random 64-byte keys. Our pricing page assumes 7x, and the query in the toolkit below measures yours. The schema can add another multiplier on top: sort order alone is close to 2x on the same rows, as the chart below shows. Codecs add a few percent at most.
Merges help too. The 10-billion-row benchmark dataset took 92 GB in six parts and 54 GB once merged into one, because bigger parts compress better. Locality-aware merges let us merge up to 150 GB parts without flooding the network. A cluster that caps part size to save bandwidth pays for it in storage.
Transfer
Egress is what you pay to read your own data from outside its region. On ClickHouse Cloud, data to the internet costs $0.1152 per GB in AWS us-east-1 and data across regions $0.0312 per GB. A dashboard pulling 1 TB a month costs $118, and replicating 10 TB a month to another region costs $319. Our pricing has no transfer charge, and we deploy in the same region as your application.
No per-query charges
Paying per scan works if you run a query a month. BigQuery charges $6.25 per TiB scanned and Snowflake charges credits per warehouse-second, multiplied by the number of clusters running. The BigQuery migration we published in June scanned about 1,600 TB a month, and at that volume the scan charge was the bill. ObsessionDB does not charge per query. The agents post covers why that matters when your client is a program that runs a hundred queries for every one a dashboard user runs.
The toolkit: what moves ClickHouse cost on any cluster
Everything above is built into our platform. The changes below work on any ClickHouse cluster, grouped by the line of the bill they reduce.
Storage
Start with the changes that cut bytes per row the most. First the sort key: put low-cardinality columns first and high-entropy ones last, so repeated values sit next to each other. Then types: use the smallest type that fits the data. Both matter more than any codec.
LowCardinality helps below about 10,000 distinct values and can hurt above 100,000, so check uniqExact on a sample first. Nullable adds a second file with a UInt8 mask, and ClickHouse's docs say it almost always hurts performance, so use a DEFAULT value where you can.
Codecs come last. Self-managed ClickHouse defaults to LZ4 and ClickHouse Cloud to ZSTD(1). ZSTD compresses about 30% better but decompresses more slowly. Delta and DoubleDelta suit increasing integers, T64 integers in a narrow range, and Gorilla slowly changing floats, but ClickHouse's own tests found they rarely beat ZSTD. Check each column before changing anything:
SELECT name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE table = 'transfers'
GROUP BY name
ORDER BY sum(data_compressed_bytes) DESC;
| Lever | What it buys | What it costs |
|---|---|---|
| Sort key: low cardinality first, entropy last | Bytes per row, by multiples | A projection or index for the other access path |
| Narrower types | About 2x on the ClickHouse docs' sample dataset | Nothing |
LowCardinality under 10K distinct values | Dictionary encoding, faster filters | Slower inserts; worse above 100K |
Int8 DEFAULT 0 in place of Nullable(Int8) | No mask file, faster scans | A sentinel the application must know |
FixedString and CODEC(NONE) on random bytes | No CPU spent on bytes that will not compress | Decoding at read time |
| A surrogate key for a repeated incompressible value | 20 to 14 TiB on one table | A lookup on the paths that need the original |
| ZSTD(1) to ZSTD(3), typed codecs on monotonic columns | The last few percent | Decompression speed above level 3 |
| A projection | Latency on a second key | About 2x the table's storage |
Compute
Use fewer, bigger nodes, and never fewer than three. Size the cache to your working set: all of the hot data for a real-time API, since the cache fills on write and evicts the oldest data first, and much less for observability or warehouse workloads. Turn parallel replicas on per query, with a minimum-rows guard. Let merges reach 150 GB parts, since the 92 GB to 54 GB drop above is free storage. Remove extra nodes when jobs finish; on shared storage that only touches metadata.
Movement
A merge downloads data, decompresses it, compresses it again and uploads it, and deletes repeat part of that work. On object storage, every step costs requests and bandwidth. A query costs at least its GET requests, as the latency post explains, and ingest costs at least the PUTs per part. Packed storage writes all of a part's columns into one object, about 15 times fewer writes for wide parts. To move less data, use the placement settings: cache_locality_aware_merges with min_bytes_for_locality_aware_merges at 100 MB, and parallel_replicas_cache_locality_strategy = 'auto'. Each materialized view adds another write per insert. For egress, traffic within a region is free on every ClickHouse service, so run compute in the same region as your application and use a private link for the rest.
Which line is growing, and how to fix it
Find the line of the bill that is growing, then use the table.
| What grew | Check first | The move |
|---|---|---|
| Storage, faster than row count | Bytes per row per column in system.columns | Sort key order, narrower types, DEFAULT in place of Nullable, LowCardinality under 10K, a surrogate for the incompressible column |
| Storage, doubled after a schema change | Projection size against the table | Budget 2x, or serve the second key with a skip index |
| Compute, with node count | Cross-node bytes during merge windows | Locality-aware merges, or fewer, bigger nodes |
| Compute, with concurrency | Cache hit rate and GETs per query | Cache sized to the working set; one distributed cache in place of per-node caches |
| A transfer line | Who reads from which region | Co-locate the compute; private links for the rest |
| A per-scan line | Queries per month against the fixed-compute price | The workload belongs on provisioned compute |
This is the same idea as the latency and scaling posts: less hardware for the same performance, or more performance from the same hardware. The bill follows the hardware. If yours keeps growing and the table does not explain why, our cost audit looks at your query log and column sizes and tells you what we find, whether you run on ObsessionDB or not.
FAQ
Because data volume is only one of four lines on the bill. Compute grows with concurrency and with the bigger nodes that bandwidth pushes you into. Storage grows with sort order and column types as much as with row count. Transfer grows with where your readers are. Check each line separately.
It depends on the data. ClickHouse's docs say around 10x for typical analytical data. We measured 16x on logs, 13x on metrics, 7.94x on a transfers partition, about 6.5x across one cluster, and 3.05x on a table full of random 64-byte signatures. Run the `system.columns` query from the toolkit on your own tables.
Yes. A Nullable column stores an extra UInt8 mask file next to the values, and ClickHouse's docs say it almost always hurts performance. One independent test on a billion rows found a 187 MB mask, about 9% more storage, and scans a third slower. Use a DEFAULT value where you can.
It saves space below about 10,000 distinct values, where a dictionary is smaller than the raw column. Above 100,000 the dictionary costs more than it saves, and ClickHouse rejects it on types of 8 bytes or less unless you override the check. Inserts also get slower, by 60% in one published test.
On ReplicatedMergeTree, yes. Each replica copies every part to its own disk or S3 objects, so three replicas store three copies. Zero-copy replication would let them share, but ClickHouse says it is not ready for production. Shared-storage engines, SharedMergeTree and ObsessionDB's Alloy, keep one copy however many nodes you run.
They charge for different things. BigQuery on-demand charges $6.25 per TiB scanned, and Snowflake charges credits per warehouse-second, multiplied by the clusters running. ClickHouse services charge for compute and compressed storage, with nothing per query. That is how a workload scanning 1,600 TB a month cost 75% less after moving.
Continue Reading
Originally written for obsessionDB. Read the original article here.
ClickHouse is a registered trademark of ClickHouse, Inc. https://clickhouse.com