Cost & performance

Beyond SQL benchmarks: the true cost of streaming analytics

An end-to-end comparison of Kafka + Spark + Iceberg, Snowflake, ClickHouse, and Streaming Lake. The real cost of streaming analytics isn't a SQL engine's raw speed — it's the bytes you read, the systems you run, and the price of every query. Here it is, backed by a runnable benchmark.

72×
less data read (1-hour window)
11×
less data read (8-hour window)
1
engine — ingest, store & query
~2.8×
cheaper — and no S3 at all

The one number that matters: bytes read

Most lakehouse cost is paid at read time — bytes pulled from storage, decompressed, and pushed through a join. The clearest way to compare architectures is therefore the simplest: how many bytes did you have to read to answer the query?

Same query, same 10 GB dataset: Spark + Iceberg read ~1.7–5 GB (whole hour/day partitions, 128 MB row groups) Streaming Lake read 128 MB (1 MB pages, pruned before decode)

Reading ~70× fewer bytes is not a micro-optimization: fewer bytes read generally means less CPU, less memory, lower latency, and lower cost, all at once. Everything below is how Streaming Lake gets there — and where the claim stops.

What we measured

The workload is the hard case: a selective inner join on fresh data, the query pattern that lakehouses handle worst.

SELECT ... FROM Person p INNER JOIN Employee e ON p.personId = e.personId WHERE p.eventTime IN [t0, t0 + window) -- prune by event time (5-min data ledgers) AND p.age <= 52 -- clustered predicate -> prune pages AND e.salary <= 63000 -- clustered predicate -> prune pages
Dataset10 GB (Person 5 GB + Employee 5 GB), event-time ordered over 2 days
Data-ledger rolloverevery 5 minutes → 576 ledgers/table, 4,608 pages/table, ~9.4M rows total
Page size1 MiB (for reference, Iceberg's default Parquet row group is 128 MiB)
Join build tableoff-heap, spilling to a real local file — memory-bounded on a single broker

Metadata (the catalog, per-page min/max + bloom footers, and immutable column segments) is built for the full layout, so pruning runs against the true dataset. Only the pages that survive pruning are decoded and joined, and the off-heap build side spills to a real file on disk — so pruning ratios, join correctness, and the off-heap working set are all measured, not estimated.

Results

Query window Data scanned I/O reduction Matches Off-heap join state Latency
1-hour 128 MiB of ~9 GB · 1.39% 72× 63,488 2.2 MB real disk 187 ms
8-hour 832 MiB of ~9 GB · 9.03% 11× 412,672 14.6 MB real disk 1,110 ms

Measured on this branch (single broker JVM). The narrower the query, the more it prunes — a 1-hour window reads 1/72nd of the data. The join's entire working set for the selective query is 2.2 MB.

Why so little I/O — pruning before you decode

Two design choices do the work:

On the probe side the join is late-materialized: only the join-key column is read per row, and the full row is reconstructed only when a key matches — so non-matching rows never pay to be decoded.

One engine, not five

The other half of the cost story is architectural. The standard stack copies data through five systems before a query can run, and pays for each one continuously:

Producer → Kafka → Spark Streaming → S3 + Iceberg → Spark SQL ▲ ▲ ▲ ▲ brokers always-on storage + shuffle · sort · ETL cluster compaction hash join per query

Streaming Lake keeps a single copy in BookKeeper and serves both pub-sub and analytics from it — no ETL job, no second store, no manifest maintenance, no object-store GET/LIST charges:

Producer → Pulsar → BookKeeper → Segment metadata → Broker query (one durable copy = stream + table + history)

Cost comparison

Modeled on published AWS list prices for a sustained pipeline (~10 MB/s ingest, 7-day hot set, ~1,000 selective joins/day). Streaming Lake numbers are the measured read behavior applied to that infrastructure; the competitor column is modeled from list prices and each system's documented defaults.

 Kafka + Spark + S3 + Iceberg modeledStreaming Lake
Monthly TCO~$7,689~$2,700all data on BookKeeper
Systems to operate51Pulsar + BookKeeper
Object storeS3 requiredIcebergnoneBookKeeper is the store
Data read / selective query2–6× more128 MB row groups128 MiB1.4% of data
Join working setshuffle across the cluster2.2 MBoff-heap, one broker
Query latencyseconds–minutesspin-up · shuffle~1 smeasured
Freshness at query timeafter sink + compactionlive

No object store, by design. Streaming Lake holds the entire dataset on BookKeeper — there is no S3 line at all. At RF-2 (Pulsar's default) on cheap sc1 Cold HDD the total is ~$2,700/mo (~2.8×); on warmer st1 HDD it's ~$4,255 (~1.8×), and RF-3 on st1 is ~$5,420 (~1.4×). Per byte, replicated HDD is dearer than S3's erasure coding — the win is deleting the object store, the ETL, and the query cluster around it, not the storage rate.

What you actually run — line by line

Streaming Lake spends money only where speed is needed: NVMe on the bookie journal and the query-broker spill, cheap Cold HDD (sc1) for the bulk ledger data, and cheap stateless pub-sub brokers. The five-component stack pays for a Kafka cluster, two always-on Spark roles, and object storage on top of each other.

Kafka + Spark + S3 + Iceberg modeled$/moStreaming Lake$/mo
Kafka (MSK) brokers + storage$2,439Pub-sub brokers cheap, stateless · 2 × m5.xlarge$280
Spark Streaming ingest always-on EMR$1,102Query brokers NVMe · runs joins + off-heap spill · 2 × r5.2xlarge$777
S3 storage Iceberg data + metadata$598Bookie compute 3 × m5.2xlarge$841
Iceberg compaction periodic Spark$550Bookie journal NVMe · 3 × 100 GB$24
Spark SQL query per query, 1,000/day$3,000Bookie ledger Cold HDD (sc1) · full 30-day dataset · RF-2 ≈ 52 TB$778
  Query execution on the query brokers above — no per-query clusterincl.
Total~$7,689Total~$2,700

AWS on-demand list prices, US-East-1 (verify current). The competitor side is modeled from list prices and documented defaults; the Streaming Lake side applies the measured read behavior to this hardware. Discounts (reserved / spot, often 30–70%) apply to both. One honest caveat: sc1 is cold HDD — cheapest per byte, but low random IOPS — so keep the fresh/hot working set on faster media (st1 or NVMe + page cache) via bookie affinity groups; cold data served from sc1 answers in seconds, not the ~1 s measured on NVMe.

How it stacks up

A qualitative read of where the selective-join workload lands across four representative systems:

AreaKafka + Spark + IcebergSnowflakeClickHouseStreaming Lake
Streaming ingestionExcellentExternal pipelineGoodExcellent
Fresh-data queryMinutescommit cadenceGoodExcellentExcellent
Predicate pruningFile + row groupMicro-partitionsMarks / granulesPage-level
Join executionDistributed shuffleDistributedLocal or distributedBroker-local + off-heap
InfrastructureKafka+Spark+S3+IcebergWarehouseClusterBroker + BookKeeper
Read amplificationMediumMediumLowPotentially very low
Operational complexityHighLow managedMediumMedium

Each system is excellent at what it was built for. ClickHouse in particular remains a best-in-class analytical engine; the distinction here is architectural — Streaming Lake unifies ingestion, storage, indexing, and query on one source-of-truth log.

What's next

To turn a strong result into an unarguable one, the roadmap for this benchmark is:

In one line: Streaming Lake integrates streaming ingestion, storage, indexing, and query execution into a single architecture that minimizes storage reads through page-level pruning — enabling selective analytical joins with small working sets, and without a separate ETL or distributed query tier.

Reproduce it

The benchmark is a normal broker test, opt-in via an environment variable, driving the same join engine that ships in the query tier:

SL_BENCH_RUN=true SL_BENCH_TOTALGB=10 SL_BENCH_WINDOWHOURS=8 \ ./gradlew :pulsar-broker:test --tests "*StreamLakeJoinBenchmark" \ --no-daemon --no-build-cache --rerun-tasks

Harness: StreamLakeJoinBenchmark. Full methodology, byte-level formats, and the complete cost model live in the project's DESIGN.md and BENCHMARK.md.

Want to see this on your data and your query shapes? We'll benchmark a selective join against your current stack.

Request early access →