A lakehouse ClickHouse estate is easy to buy and hard to own. You can license certified builds and a support desk, or you can pay per-second for a managed cloud that scales compute against object storage. What neither purchase gives you is a written commitment that yesterday’s events are queryable within 15 minutes on the warm tier, that a Q1 dashboard class holds p95 under 300 ms on the hot tier, and that the blended cost per terabyte-month bends downward at the 7-day and 90-day boundaries.
ChistaDATA DAPE (Data Analytics Platform Engineering) sells exactly that: lakehouse ClickHouse delivered as an engineered platform with signed SLOs for freshness across tiers, p95/p99 latency per tier and query class, and a measured cost curve.
This post explains what those three SLOs are, how they are measured from system.* tables, what the platform has to look like for them to be achievable on 100% open-source ClickHouse 26.3 LTS, and — factually, from their own published material — why the two vendors most often compared with us do not sell the same thing.
What lakehouse ClickHouse means in a DAPE engagement
In our usage, lakehouse ClickHouse is a three-tier topology in which ClickHouse MergeTree is the serving engine for hot and warm data and open table formats on object storage — Apache Iceberg with Parquet — hold the cold horizon. The tiers are not three products; they are one storage policy, one TTL clause, and one export path, and every one of them carries its own SLO envelope.
- Hot tier — ReplicatedMergeTree on local NVMe, 0–7 days, serves the latency-critical query classes. Sort key and partitioning are validated against
system.query_logpredicates, never against the data model alone. - Warm tier — the same MergeTree table, parts moved by
TTL … TO VOLUMEonto an S3-backed disk fronted by a local cache. This is where most of the retention lives and where the cost curve first bends. - Cold tier — Parquet files under an Iceberg catalog on S3, queried through the
IcebergS3engine for audit, backfill and compliance classes. ClickHouse is treated as an Iceberg reader in production designs; upstream IcebergINSERTsupport has existed since 25.8 behind an experimental/beta setting and is not on the signed path.
The reason to write lakehouse ClickHouse down as an engineered platform rather than a reference architecture is that each tier fails differently. Hot-tier freshness fails on merge backlog and part-count explosions; warm-tier latency fails on S3 GET amplification when the cache is undersized or the sort key is wrong for the class; cold-tier freshness fails silently when an export job misses a partition. An SLO per tier makes each failure mode a measured, owned number.
Why software-plus-support and cloud consumption stop at the lakehouse ClickHouse platform boundary
This is not a criticism of either vendor; it is a description of what their commercial form can and cannot commit to. We validated each statement against the vendor’s own pages in August 2026.
| Commercial form | What is sold | What the contract commits to | What it cannot sign |
|---|---|---|---|
| Altinity Stable Builds + support | Certified ClickHouse binaries maintained for up to three years; enterprise support subscriptions; Altinity.Cloud and BYOC; Project Antalya for Iceberg/Parquet tiering | Build lifecycle, security fixes, support response — the software and the people behind it | Freshness, per-tier percentiles, or cost curve on your workload — the vendor does not own your schema, ingestion shape, or storage policy |
| ClickHouse Cloud consumption | Compute and storage metered separately with autoscaling; SharedMergeTree; ClickPipes; Warehouses | An uptime SLA with service credits for HA-configured services | Query latency or data freshness — the SLA text scopes itself to availability; the cost trajectory is a function of your consumption, not a commitment |
| ChistaDATA DAPE | The engineered platform: schema, tiering, ingestion, Keeper, observability and the SLO ledger — on 100% open-source ClickHouse, in your VPC or data centre | Signed freshness, p95/p99 per tier and class, and a cost curve re-baselined quarterly, with error-budget policy and credits | Anything we have not measured on your estate first — the first 30 days establish the baseline the numbers are signed against |
Two technical facts underpin the table and explain why a lakehouse ClickHouse SLO has to be sold with the platform. First, SharedMergeTree is a different storage engine from ReplicatedMergeTree, so a Cloud sizing model and its merge behaviour do not transfer to a self-hosted estate; a vendor whose SLA is written for one engine cannot underwrite the other.
Second, an SLO on latency is only meaningful once a query class is defined against a sort key, a projection strategy and a tier — decisions that sit in the customer’s schema, which is precisely the layer a software licence or a consumption meter does not touch.
The three signed lakehouse ClickHouse SLOs, as engineering objects
Every DAPE lakehouse ClickHouse contract carries a machine-readable SLO schedule. The excerpt below is the shape; the numbers in a real schedule are the customer’s measured values after the baseline period, pinned to the LTS they were measured on.
# CD-SLO-2026-08-<CUSTOMER> — signed SLO schedule (excerpt)
platform: lakehouse-clickhouse
clickhouse_version: "26.3 LTS" # every SLO is pinned to the LTS it was measured on
measurement_window: 28d # rolling; attested monthly
tiers:
hot:
storage: nvme_replicated_mergetree
retention: 0-7d
freshness_slo: { target_seconds: 30, objective: 0.999 } # 99.9% of 60 s samples ≤ 30 s
latency_slo:
class_q1_dashboards: { p95_ms: 300, p99_ms: 800, objective: 0.99 }
class_q2_adhoc: { p95_ms: 2000, p99_ms: 5000, objective: 0.95 }
cost_curve: { usd_per_tb_month: "<measured>", usd_per_1k_queries_q1: "<measured>" }
warm:
storage: s3_backed_mergetree
retention: 7-90d
freshness_slo: { target_seconds: 900, objective: 0.999 }
latency_slo:
class_q2_adhoc: { p95_ms: 2000, p99_ms: 5000, objective: 0.95 }
cost_curve: { usd_per_tb_month: "<measured>" }
cold:
storage: iceberg_parquet_s3
retention: 90d-7y
freshness_slo: { target_seconds: 86400, objective: 0.99 }
latency_slo:
class_q3_audit: { p95_ms: 30000, p99_ms: 90000, objective: 0.95 }
cost_curve: { usd_per_tb_month: "<measured>" }
error_budget_policy:
burn_rate_alerts: [ { window: 2h, factor: 14.4 }, { window: 24h, factor: 3 } ]
exhausted: "feature freeze on the tier; ChistaDATA engineering time re-prioritised to the SLO"
attestation:
evidence_tables: [ slo.sli_samples, slo.slo_rollup_28d ]
signed_by: [ customer_data_platform_owner, chistadata_engagement_lead ]SLO 1 — Freshness across tiers
In a lakehouse ClickHouse platform, freshness is defined per tier as the age of the newest row that is visible in that tier’s active parts — for the hot tier relative to now, for the warm tier relative to the MOVE TTL boundary — sampled every 60 seconds. It is not an ingestion-pipeline metric borrowed from Kafka lag; it is measured where the query runs. On 26.3 LTS async_insert is enabled by default, so the hot-tier freshness budget must explicitly absorb the buffer-to-flush interval, and wait_for_async_insert = 1 is the durability contract we state to application teams.
-- SLI: freshness per tier. Hot lag = now() - newest event visible in hot parts.
-- Warm lag = how far the warm tier trails the 7-day MOVE TTL boundary (0 when the
-- move pipeline is keeping up). Parts are resolved to disks via the _part virtual
-- column, so the number is measured where the query runs, not in the pipeline.
WITH
now64(3) AS ts,
ts - INTERVAL 7 DAY AS warm_boundary
SELECT
p.tier,
if(p.tier = 'hot',
ts - max(e.event_time),
greatest(0, warm_boundary - max(e.event_time))) AS freshness_lag_s,
uniqExact(e._part) AS active_parts
FROM analytics.events_local AS e
INNER JOIN
(
SELECT
name,
multiIf(disk_name = 'hot_nvme', 'hot',
disk_name = 'warm_s3_cache', 'warm', 'other') AS tier
FROM system.parts
WHERE database = 'analytics'
AND table = 'events_local'
AND active
) AS p ON e._part = p.name
WHERE e.event_date >= today() - 9 -- prune to the boundary window
GROUP BY p.tier;
-- Ingest-side freshness for async inserts (26.3 LTS default-on):
-- time from first buffered row to flush, i.e. the latency the SLO must absorb.
SELECT
quantileTDigest(0.95)(flush_time - event_time) AS p95_flush_s,
quantileTDigest(0.99)(flush_time - event_time) AS p99_flush_s,
countIf(status != 'Ok') AS failed_flushes
FROM system.asynchronous_insert_log
WHERE database = 'analytics'
AND table = 'events_local'
AND event_time >= now() - INTERVAL 1 HOUR;Warm-tier freshness has a second component: the time between a part becoming eligible for MOVE TTL and its arrival on the S3 volume, visible in system.moves and system.part_log with event_type = 'MovePart'. Cold-tier freshness is the age of the newest committed Iceberg snapshot relative to the newest MergeTree partition that has aged past the warm boundary. Each of the three is a number, each has a target, and each has an objective (the fraction of samples that must meet the target).
SLO 2 — p95 and p99 per tier and per query class
A lakehouse ClickHouse latency SLO without a query class is a marketing number. DAPE requires every client to tag its queries with log_comment, which flows into system.query_log unchanged and lets the SLI be sliced by class and tier. Percentiles are computed with quantileTDigest over the raw log across all replicas, not from a sampled dashboard.
-- SLI: p95/p99 per query class. Class comes from the mandatory log_comment
-- every DAPE client sets (e.g. SETTINGS log_comment = 'class=q1;tier=hot').
SELECT
extract(log_comment, 'class=(\\w+)') AS query_class,
extract(log_comment, 'tier=(\\w+)') AS tier,
count() AS queries,
quantileTDigest(0.95)(query_duration_ms) AS p95_ms,
quantileTDigest(0.99)(query_duration_ms) AS p99_ms,
sum(read_bytes) / 1e9 AS read_gb,
sum(ProfileEvents['S3GetObject']) AS s3_gets,
sum(ProfileEvents['SelectedMarks']) AS selected_marks
FROM clusterAllReplicas('{cluster}', system.query_log)
WHERE type = 'QueryFinish'
AND query_kind = 'Select'
AND event_time >= now() - INTERVAL 1 HOUR
AND log_comment LIKE 'class=%'
GROUP BY query_class, tier
ORDER BY tier, query_class;The two ProfileEvents in that query are the diagnostic edge of the SLO. A warm-tier class whose S3GetObject count climbs while SelectedMarks stays flat is a cache-sizing problem; a hot-tier class whose SelectedMarks climbs is a sort-key or projection problem. Both are engineering actions we own under the contract, and both are diagnosed in the same table the SLO is measured from — the approach we describe in ClickHouse query optimisation for petabyte-scale analytics.
SLO 3 — The cost curve
The lakehouse ClickHouse cost curve is the blended dollars per terabyte-month as a function of data age, plus dollars per thousand queries for each class. It is derived from system.parts compressed bytes per disk joined to the unit prices on the customer’s own cloud invoice. It is signed as a shape with re-baselining every quarter, because unit prices, compression ratios and the hot/warm boundary all move.
-- Cost curve input: compressed bytes per tier, joined to the unit price per disk.
-- Unit prices come from the customer's invoice; nothing here is list-price arithmetic.
SELECT
p.disk_name AS disk,
round(sum(p.bytes_on_disk) / 1e12, 3) AS tb_on_disk,
round(sum(p.data_uncompressed_bytes) / sum(p.bytes_on_disk), 2) AS compression_ratio,
u.usd_per_tb_month,
round(tb_on_disk * u.usd_per_tb_month, 2) AS usd_month
FROM clusterAllReplicas('{cluster}', system.parts) AS p
INNER JOIN slo.unit_prices AS u ON u.disk_name = p.disk_name
WHERE p.active
AND p.database = 'analytics'
GROUP BY disk, u.usd_per_tb_month
ORDER BY usd_month DESC;The point of signing a lakehouse ClickHouse cost curve rather than a single number is that the hot/warm boundary is the most expensive knob on the platform. Pulling it from 7 days to 3 days halves hot-tier NVMe spend and pushes the Q2 ad-hoc class onto S3 reads; the latency SLO for that class decides whether the move is allowed. The three SLOs constrain each other on purpose.
How lakehouse ClickHouse SLOs are measured, attested and enforced
The lakehouse ClickHouse SLO ledger is a ClickHouse table. It is replicated, partitioned by month, retained for thirteen months so a full year of attestations is always reproducible, and it is the only source both signatures refer to.
-- SLO ledger and 28-day error budget. The ledger is a ReplicatedMergeTree table
-- in the customer's own estate, so both parties attest against identical evidence.
CREATE TABLE slo.sli_samples ON CLUSTER '{cluster}'
(
sampled_at DateTime('UTC'),
tier LowCardinality(String),
sli LowCardinality(String), -- 'freshness_s' | 'p95_ms' | 'p99_ms'
query_class LowCardinality(String),
value Float64,
target Float64,
good UInt8 MATERIALIZED value <= target
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/slo/sli_samples', '{replica}')
PARTITION BY toYYYYMM(sampled_at)
ORDER BY (tier, sli, query_class, sampled_at)
TTL sampled_at + INTERVAL 13 MONTH DELETE
SETTINGS index_granularity = 8192;
-- Remaining error budget per (tier, sli, class) over the attestation window.
WITH 0.999 AS objective
SELECT
tier,
sli,
query_class,
count() AS samples,
countIf(good = 0) AS bad_samples,
round(1 - bad_samples / samples, 5) AS achieved,
round((1 - objective) * samples) AS budget_samples,
budget_samples - bad_samples AS budget_remaining
FROM slo.sli_samples
WHERE sampled_at >= now() - INTERVAL 28 DAY
GROUP BY tier, sli, query_class
ORDER BY budget_remaining ASC;Lakehouse ClickHouse error-budget policy follows the multi-window burn-rate pattern documented in the Google SRE Workbook: a fast window (2 hours at 14.4× burn) pages, a slow window (24 hours at 3×) opens a ticket. When a tier’s budget is exhausted, the contract triggers a feature freeze on that tier and reprioritises ChistaDATA engineering time to the SLO. This is the same discipline we run as Data SRE for ClickHouse, applied to a commercial commitment.
What a lakehouse ClickHouse platform has to look like for the SLOs to be achievable
Signing lakehouse ClickHouse numbers is only responsible if the platform is engineered to hit them. The following are the non-negotiable design elements in a DAPE lakehouse ClickHouse build on 26.3 LTS.
Storage policy and TTL define the tier boundary
<!-- /etc/clickhouse-server/config.d/storage.xml (restart required for new disks) -->
<clickhouse>
<storage_configuration>
<disks>
<hot_nvme>
<path>/var/lib/clickhouse/hot/</path>
</hot_nvme>
<warm_s3>
<type>s3</type>
<endpoint>https://s3.${AWS_REGION}.amazonaws.com/${CH_WARM_BUCKET}/${CH_CLUSTER}/{replica}/</endpoint>
<access_key_id>${CH_S3_ACCESS_KEY}</access_key_id>
<secret_access_key>${CH_S3_SECRET_KEY}</secret_access_key>
<metadata_path>/var/lib/clickhouse/disks/warm_s3/</metadata_path>
</warm_s3>
<warm_s3_cache>
<type>cache</type>
<disk>warm_s3</disk>
<path>/var/lib/clickhouse/cache/warm_s3/</path>
<max_size>400Gi</max_size>
</warm_s3_cache>
</disks>
<policies>
<tiered_hot_warm>
<volumes>
<hot><disk>hot_nvme</disk></hot>
<warm><disk>warm_s3_cache</disk></warm>
</volumes>
<move_factor>0.10</move_factor>
</tiered_hot_warm>
</policies>
</storage_configuration>
</clickhouse>-- Hot + warm tiers in one table; the tier boundary is a TTL expression, not an ETL job.
CREATE TABLE analytics.events_local ON CLUSTER '{cluster}'
(
event_date Date CODEC(DoubleDelta, ZSTD(1)),
event_time DateTime64(3, 'UTC') CODEC(DoubleDelta, ZSTD(1)),
tenant_id UInt32,
event_type LowCardinality(String),
entity_id UInt64,
payload JSON, -- JSON is GA since 25.3
INDEX idx_entity entity_id TYPE bloom_filter(0.01) GRANULARITY 4
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/analytics/events_local', '{replica}')
PARTITION BY toYYYYMMDD(event_date)
ORDER BY (tenant_id, event_type, event_time)
TTL event_date + INTERVAL 7 DAY TO VOLUME 'warm',
event_date + INTERVAL 90 DAY DELETE -- cold copy is exported before this fires
SETTINGS
storage_policy = 'tiered_hot_warm',
index_granularity = 8192,
min_bytes_for_wide_part = 10485760,
ttl_only_drop_parts = 1; -- whole-part drops: cheap, predictable
-- Cold tier: Iceberg on S3, read-only from ClickHouse in production designs.
-- Iceberg INSERT support exists upstream since 25.8 behind an experimental/beta
-- setting; the DAPE cold path uses Parquet export + catalog commit instead.
CREATE TABLE analytics.events_cold
ENGINE = IcebergS3(
'https://s3.${AWS_REGION}.amazonaws.com/${CH_COLD_BUCKET}/warehouse/analytics/events/',
'${CH_S3_ACCESS_KEY}', '${CH_S3_SECRET_KEY}',
'Parquet'
);The S3 disk is wrapped in a cache disk sized from the warm-class working set (read_bytes per class over 7 days from system.query_log), not from a rule of thumb. ttl_only_drop_parts = 1 keeps the 90-day delete cheap. Zero-copy replication is not used: allow_remote_fs_zero_copy_replication remains experimental upstream, so warm-tier replicas hold their own S3 objects and the cost curve is computed on that basis. Full storage-policy semantics are in the MergeTree documentation.
Ingestion shape is designed with the freshness SLO
- Batches of 10k–500k rows per INSERT, enforced server-side by
async_inserton 26.3 LTS, withasync_insert_busy_timeout_msset below the hot-tier freshness target minus merge headroom. - Dedup semantics derived per estate from
system.asynchronous_insert_logandsystem.part_logbefore any exactly-once claim; 26.x changed the default dedup surface and the docs are not fully reconciled. - Kafka via an external consumer or sink connector rather than the Kafka table engine, which remains labelled experimental; the design pattern is in ClickHouse Kafka real-time analytics.
Query classes choose projections or parallel replicas, not both
Projections remain incompatible with parallel replicas as of 26.7. Each query class in the SLO schedule therefore declares which mechanism serves it, and the choice is verified with EXPLAIN PIPELINE before the class is signed. Hot-tier dashboard classes typically get narrow index-shaped projections; warm-tier ad-hoc classes lean on parallel replicas across the S3-backed replicas.
Keeper is sized and drilled as tier-0
The 26.2/26.3 dedup and async-insert default changes moved Keeper load into the first-order risk register. Every DAPE estate baselines system.zookeeper_log request rates and system.zookeeper_info before any upgrade and re-compares within one hour after. Keeper is upgraded first and separately; a freshness SLO cannot survive a coordination outage.
Cold tier: reader in production, writer under labelled acceptance
In the lakehouse ClickHouse cold tier, data reaches Iceberg through a Parquet export and catalog commit, with a reconciliation query comparing count() and max(event_time) per partition between the warm MergeTree table and the IcebergS3 engine view before the MergeTree partition is allowed to age out. For estates that want MergeTree-to-Iceberg tiering natively, Altinity Project Antalya is the credible open-source path and we will run it where a customer accepts its experimental flags; the signed cold-tier SLO does not depend on it.
Where DAPE lakehouse ClickHouse does not apply, and what we do not sign
- Estates on ClickHouse Cloud. SharedMergeTree behaviour, Warehouses and ClickPipes are outside our measurement surface. We will design the OSS-portable half of such an estate and state the exit cost of the cloud-only half, but we do not sign SLOs on an engine we do not operate.
- Unclassified workloads. If a customer cannot or will not tag queries by class, there is no latency SLO — only the freshness and cost SLOs are offered until classification is in place.
- Numbers before baseline. The first 30 days of any engagement are measurement. Targets proposed before that period are labelled illustrative, as they are in every figure above.
- Versions we have not soaked. SLOs are pinned to an LTS. A move to a non-LTS stable resets the attestation window until the platform has soaked on it.
- Regulated builds. Where FIPS-validated binaries are contractual, the available Altinity Stable variants do not currently include 25.8 or 26.3; that becomes a compliance-versus-currency decision taken with the customer before any number is signed.
Lakehouse ClickHouse with signed SLOs: frequently asked questions
Is DAPE a competitor to Altinity or ClickHouse Cloud?
DAPE lakehouse ClickHouse occupies a different layer. Altinity’s builds and operator, and ClickHouse Inc.’s open-source engine, are inputs to a DAPE platform; we operate Altinity’s clickhouse-operator and clickhouse-backup where they fit. What DAPE adds is the engineered platform above them and the signed SLOs that a software licence or a consumption meter cannot carry.
What versions of ClickHouse does the signed platform run on?
A DAPE lakehouse ClickHouse platform tracks the current LTS — 26.3 LTS as of August 2026, with 25.8 LTS reaching upstream end of support at the end of the month. Change-frozen estates may run Altinity Stable 25.8 under its extended lifecycle; the SLO schedule records the exact build it was measured on.
Can the cost curve include the ClickHouse cloud bill for the warm and cold tiers?
Yes. The slo.unit_prices table is loaded from the customer’s actual invoice lines for NVMe, S3 storage, S3 requests and egress, and the curve is recomputed monthly against those prices. The curve is signed as a shape and re-baselined quarterly.
How is a missed SLO handled commercially?
Error-budget exhaustion triggers a feature freeze on the affected tier and reprioritises ChistaDATA engineering to the SLO. Persistent misses across an attestation period carry service credits defined in the contract. Both parties attest against the same ledger table, so there is no dispute over whose dashboard is right.
Next step
If you run, or are about to build, a lakehouse ClickHouse estate and want freshness, per-tier percentiles and a cost curve you can sign rather than hope for, start with a baseline. ChistaDATA ClickHouse consulting scopes the 30-day measurement period; ClickHouse managed services carries the platform and its SLOs after that. Standing caveat for everything above: test every change in staging before production, keep a verified restore drill in the calendar, and treat the numbers in this post as illustrative until they have been measured on your estate.