✦ Yeedu Hits $0.53/TB in a TPC-DS Benchmark
Check-with-circle-green-icon
Blog
Yeedu Team
August 6, 2026

Apache Iceberg compaction: the small-files tax that can cost more than the disease it treats

Apache Iceberg compaction: the small-files tax that can cost more than the disease it treats

Compaction gets pitched as routine table hygiene, the Iceberg equivalent of vacuuming a Postgres table. It isn't. Run it on a fixed schedule with default settings and it becomes a second, poorly understood cost center: one that can run 20 to 30 times more expensive than a tuned job and still lag hours behind the ingestion it's supposed to be cleaning up after.

We've watched teams treat rewrite_data_files as a checkbox: schedule it nightly, move on. Then the compaction cluster shows up as a line item bigger than the query compute it was meant to protect, and nobody can explain why.

Why small files accumulate faster than teams expect

Every streaming or micro-batch writer commits a new snapshot on each flush. Iceberg tracks every one of those commits faithfully, which is the whole point of the format, and also the source of the problem.

A streaming job checkpointing every few seconds doesn't just produce small data files. It produces a long chain of snapshots, each with its own manifest update, and that metadata churn compounds independently of file count.

Fixing small files in Iceberg tables means treating commit frequency as a first-order cost driver on streaming tables, not an afterthought tuned after the fact.

Delete operations make it worse. Iceberg implements updates as a delete plus an insert rather than an in-place rewrite, so a table under moderate churn generates data files and delta files simultaneously.

Starburst's TPC-DS benchmark found that once 3% of rows in a table had gone through delete-and-insert cycles, query performance dropped by a factor of 1.5x, purely from the resulting file explosion. Every file, however small, carries a fixed open cost. A single scan can explode into thousands of file-open operations before a single byte of relevant data gets read.

Why does query planning slow down before query execution even starts?

Planning time grows with file and manifest count, not with the size of the result set. Each additional small file adds a fixed per-file cost to opening, listing, and reading manifest entries, so a table fragmented into thousands of small files pays that tax before a single predicate gets evaluated.

What actually triggers the small-files problem in practice?

Three patterns dominate: frequent trickle-load commits from streaming writers, derived datasets persisted without regard for downstream file layout, and delete/merge-heavy workloads that pile up delta files faster than compaction clears them.

Amazon's EMR team demonstrated the fix's payoff directly: compacting 58,176 small objects totaling 2GB into roughly 437MB files cut a representative query's runtime from 1 minute 39 seconds to 59 seconds, a 40% improvement, using EMR's event-based compaction triggered after 10 successful commits.

That's the upside case. It's also the case everyone quotes. The downside case gets talked about far less.

Where compaction jobs quietly become the expensive part

Here's the part that surprises people who've only read the marketing docs: max-concurrent-file-group-rewrites often sits at just 1 on tables nobody has retuned, leaving compaction to run sequentially in practice.

Iceberg's rewrite planner groups files, then parallelizes across groups, not across individual files. If 10,000 small files get bucketed into five file groups, you get exactly five parallel tasks no matter how large the cluster is. One executor churns while the rest sit idle.

How the file-group bottleneck produces one busy executor regardless of cluster size:

Iceberg's rewrite planner parallelizes across file groups, not individual files, so one busy executor churns regardless of cluster size
10,000 small files bucketed into five file groups means exactly five parallel tasks, no matter the cluster size

IOMETE's breakdown of this bottleneck calls it a structural constraint, not a tuning failure. It's how Iceberg avoids extreme shuffle pressure and preserves atomic commits by design, and it means naive scaling (bigger cluster, same config) doesn't touch the actual bottleneck.

Z-order sorting compounds the risk. Applying it to a 200GB+ partition is, according to the same analysis, the single most common cause of out-of-memory failures in Spark-based Iceberg maintenance jobs.

The managed-compaction premium

Managed compaction doesn't sidestep this, it can amplify it. Onehouse benchmarked AWS S3 Tables' managed maintenance against a DIY EMR job on a 100GB, 10,000-file dataset.

S3 Tables running roughly 29x more expensive than EMR for the identical compaction outcome:

Compaction approachDatasetCostCost vs. cheapest option
AWS EMR (self-managed Spark)100GB, ~10K files$0.171x (baseline)
Onehouse100GB, ~10K files$2.29~13.5x
AWS S3 Tables (managed)100GB, ~10K files$5.04~29.6x

On a larger 953.7GB run, the comparison against Onehouse's own engine showed the same pattern: $47.69 for S3 Tables against $2.29, roughly 20x.

Worse, it wasn't fast. Observability was close to nonexistent: no dashboard, just a CLI command showing the timestamp of the last run. Teams paying a premium for "managed" got neither speed nor visibility in return.

Why does the JVM keep showing up as the bottleneck?

Spark's JVM startup and task-coordination overhead, what RisingWave's engineering team calls the "distributed tax," dominates short compaction jobs where actual data movement is small relative to orchestration cost.

EngineWall-clock timeCompute cost
Apache Spark1,533 seconds (~25.5 min)$0.33
Rust/DataFusion (RisingWave)277 seconds (~4.6 min)$0.06

That's a 5.5x gap in time and an 82% reduction in compute cost for the identical bin-pack workload.

On a harder workload mixing 20,000 data files with 20,000 position deletes and 20,000 equality deletes, Spark failed with out-of-memory errors on the same hardware where the Rust engine succeeded.

We're not suggesting every team rip out Spark for compaction. Most shops already run Spark for everything else, and adding a second execution engine has its own operational cost. But it's worth knowing that the JVM overhead isn't imaginary: it's measured, it's repeatable, and it's the reason a five-minute compaction job can burn fifteen minutes of wall clock on coordination alone.

At what point does maintenance spend indicate a broken strategy, not just a scaling issue?

One rule of thumb from production lakehouse operators: if the compaction cluster's bill exceeds roughly 30% of query compute spend, the strategy itself, not the cluster size, needs to change.

LakeOps' FinOps analysis of Iceberg fleets across several production deployments found that 25 to 40% of object-storage spend on typical lakes covers bytes no query will ever read again, orphaned or superseded files left behind by compaction runs that never got followed up with snapshot expiration.

Table layout itself carries a startling multiplier. The same analysis cites a 1TB table with a selective filter costing about $0.25 to scan when well laid out, versus $5.00 for the same query against a fragmented, unsorted copy. That's a 20x difference driven entirely by file layout, before anyone touches a WHERE clause.

Sequencing compaction and cleanup in the right order

Sequencing matters, and it's an easy mistake to make: running expire_snapshots ahead of rewrite_data_files clears snapshot metadata but skips over data files that compaction hasn't superseded yet, so much of the intended cleanup never actually happens. In practice, a maintenance pass on a table under active write pressure should run in this order:

  1. Run rewrite_data_files on the partitions showing small-file or delete-file buildup, so small files get bin-packed and outdated data files get superseded.
  2. Confirm the new snapshot has committed successfully before touching cleanup operations.
  3. Run expire_snapshots now that the files it's cleaning up have actually been superseded, rather than before compaction has run.
  4. Trigger the next compaction pass off a delete-file-count or small-file-count threshold rather than a fixed clock, so tables with heavy churn don't sit exposed for a full schedule cycle while quiet tables aren't compacted needlessly.

Why maintenance order matters: running cleanup before compaction leaves superseded files stranded:

Correct maintenance order: run rewrite_data_files before expire_snapshots so cleanup doesn't skip files compaction hasn't superseded yet
Running expire_snapshots before rewrite_data_files leaves superseded files stranded instead of cleaned up

Preventing the problem instead of paying to fix it

Compaction treats a symptom. The disease is commit frequency and file-size configuration at write time, and it's cheaper to fix there. Set write.target-file-size-bytes (or the Databricks equivalents, delta.targetFileSize and iceberg.targetFileSize) explicitly rather than relying on defaults.

Configuration knobs worth setting explicitly

Databricks auto-tunes based on table size: 256MB for tables under 2.56TB, scaling linearly to 1GB between 2.56TB and 10TB, and 1GB beyond that, via its file size controls.

Auto-compaction has its own knobs, spark.databricks.iceberg.autoCompact.enabled and its Delta counterpart, distinct from the manual OPTIMIZE/rewrite_data_files path, and mixing the two without understanding which one owns a given table is a common source of duplicated, wasted work.

Streaming writers deserve particular scrutiny. Lengthening checkpoint intervals reduces snapshot count directly. A table committing every 10 minutes can accumulate over 4,000 snapshots a month; stretching that interval doesn't just reduce data files, it thins the manifest chain that query planning has to traverse on every read.

None of this argues against compaction. Iceberg needs it, and skipping it entirely just relocates the cost to every downstream query, forever, instead of paying it once on a schedule you control.

It argues against treating the default configuration as good enough for a production table under real write pressure. The teams getting burned aren't running compaction wrong so much as running it blind: default concurrency, default schedule, no upstream tuning, and a bill that shows up looking like someone else's problem.

Back to blogs
Join our Insider Circle
Get exclusive content crafted for engineers, architects, and data leaders building the next generation of platforms.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
No spam. Just high-value intel.
Back to blogs