Back to blog

Data Lakehouse Maintenance with Airflow: Why It Breaks

Most data lakehouse teams start maintaining Iceberg tables with Airflow DAGs and Spark SQL procedures. This guide covers the five structural pitfalls that emerge at scale — fixed schedules, per-table DAGs, JVM overhead, missing coordination, and blind-spot observability — and the autonomous control plane architecture that replaces them.

Jonathan Saring
Data PlatformsApache AirflowData LakehouseApache IcebergCompactionLakeOpsLakehouse MaintenanceIceberg Maintenance

Jonathan Saring

17 min read
Data Lakehouse Maintenance with Apache Airflow — crystalline lakehouse on a floating island with the Airflow logo and Iceberg emblem

If you run a data lakehouse on Apache Iceberg, you are almost certainly maintaining it with Apache Airflow. A DAG that calls Spark SQL procedures — rewrite_data_files, expire_snapshots, remove_orphan_files, rewrite_manifests — scheduled nightly at 2 AM. Maybe a second DAG for the streaming tables that need more frequent compaction. A third for the compliance namespace where snapshot retention is 90 days instead of 7.

This setup works. At 10 tables, it works well. At 50 tables across two catalogs, it starts to strain. At 200 tables with mixed streaming and batch workloads, multiple query engines, and a team that has turned over twice since the original DAGs were written — it becomes the single largest source of on-call pages, wasted compute, and silent performance degradation in your lakehouse. The gap is not operational — it is architectural. Airflow is a workflow orchestrator being asked to be a maintenance control plane.

This guide dissects the five structural pitfalls, explains why each is inherent to the orchestrator model (you cannot fix them with better DAGs), and at each step shows how a purpose-built control plane resolves the problem by design. If your Airflow maintenance still works, this guide tells you what will break and when. If it has already broken, it maps the path forward.

LakeOps is the autonomous lakehouse control plane referenced throughout — a Rust-based engine built on Apache DataFusion that connects to your existing Iceberg catalogs (Glue, REST/Polaris, Nessie, S3 Tables) and query engines without moving data or changing pipelines. What makes it fundamentally different from better DAGs: it monitors every table's structural health continuously, fires maintenance when tables actually need it (not on a cron), sequences operations correctly across the entire lake, avoids conflicts with production writers through partition-level awareness, runs compaction 95% faster than Spark at a fraction of the cost on a native Rust engine, and sorts data files based on cross-engine query telemetry so your queries skip 90%+ of files before reading any data. The five pitfalls below each highlight where this architectural difference matters most.

LakeOps Dashboard — lake-wide health, storage trends, and optimization throughput
The LakeOps dashboard — Healthy, Warning, and Critical tables at a glance, storage trends, optimization throughput. Every table monitored continuously, maintenance triggered by health signals, not cron schedules.

Why every lakehouse needs active maintenance

Apache Iceberg gives your lakehouse ACID transactions, schema evolution, time travel, and partition evolution — all in a format readable by Spark, Trino, Flink, Snowflake, DuckDB, Athena, and every other major engine. What Iceberg does not give you is a system that keeps tables healthy over time.

Every write creates a new snapshot, adds manifest entries, and produces data files. A streaming pipeline committing every 60 seconds to 100 partitions generates 144,000 new files per day. Each file is typically 1–50 MB — far below the 256 MB–1 GB range where query engines perform efficiently. The manifests that index those files fragment. Snapshots accumulate, pinning references to superseded data. Failed writes and aborted jobs leave orphan files on storage that no snapshot references but S3 still bills for.

Left unmanaged, this entropy is not gradual. A table that performs well in week one can degrade 5–10x in query speed by week eight. The degradation is silent — no error, no alert, just Trino scans that used to take 4 seconds now taking 40, and a storage bill that grows 30% month-over-month with no new data.

The maintenance operations are well-defined. The Apache Iceberg documentation specifies four procedures that address four layers of table debt:

OperationWhat it doesWhy it matters
Snapshot expirationRemoves old snapshots and releases file referencesUnbounded snapshots bloat metadata and slow planning
Orphan file cleanupDeletes unreferenced files from storageOrphans can account for 25–40% of storage cost
Data file compactionMerges small files into optimally sized filesSmall files cause full-table scans instead of pruning
Manifest rewritingConsolidates fragmented manifest filesFragmented manifests slow query planning across all engines

The operations must run in a specific sequence. Expire snapshots first to release metadata references. Then clean orphans — now safe because expired snapshots no longer reference them. Then compact data files — on a clean state, so you never rewrite files about to be garbage-collected. Then rewrite manifests — on the post-compaction file set, so the metadata reflects the actual layout.

The question is not what to run — the procedures are clear. The question is how to orchestrate them: on a fixed schedule via Airflow DAGs, or continuously via a control plane that monitors table health and fires each operation when the table actually needs it. Airflow is the natural starting choice — it is already in your stack, it handles scheduling and retries, and the Spark SQL procedures are the documented execution path. The combination works until it does not. The five pitfalls below explain why.

Pitfall 1: Fixed schedules ignore table state

An Airflow DAG runs on a cron schedule. The schedule is static — defined when the DAG is written, occasionally updated by hand. The table's actual state is irrelevant to when maintenance runs.

A streaming table that accumulated 50,000 small files in 6 hours waits until the 2 AM cron fires. A batch table that received no new data since last week gets compacted anyway — consuming a Spark cluster for 15 minutes to discover there is nothing to do. A table that crossed the compaction threshold at 10 AM will not be compacted until 2 AM the next day, during which every query against it pays the full scan penalty of fragmented files.

The mismatch compounds with heterogeneous workloads. In any production lakehouse, tables have different write velocities, different file sizes, different retention requirements, and different query patterns. A single schedule — or even a handful of schedule tiers (hourly, daily, weekly) — cannot match the granularity of what each table actually needs. The result is a permanent gap between when maintenance should run and when it does.

LakeOps solves this with Adaptive Maintenance — continuous monitoring of table-level structural signals (file count, average file size, snapshot depth, manifest fragmentation, delete-file ratio) that triggers operations when thresholds are crossed. A streaming table that fragments every hour gets compacted every hour. A batch table that loads once a day gets compacted after ingestion. A table that received no new data is left alone entirely. Every table gets exactly the maintenance it needs, when it needs it — without a human deciding when to schedule it.

LakeOps compaction — small files through optimization to a healthy lakehouse
LakeOps compaction fires when the table needs it — a streaming table that fragments hourly is compacted hourly, a batch table is compacted after ingestion. The Rust engine runs without JVM overhead, cluster provisioning, or cron schedules.

Pitfall 2: Per-table DAGs do not scale

At 10 tables, you maintain 10 maintenance DAGs (or one parameterized DAG with 10 configurations). The configurations differ: streaming tables need hourly compaction with aggressive file targets; batch tables need daily compaction with larger targets; compliance tables need 90-day snapshot retention instead of 7.

At 100 tables across 3 catalogs, you maintain 100 configurations with different schedules, different retention windows, different compaction strategies, different where clauses for partition-scoped compaction, and different target file sizes. Every new table requires a new configuration. Every schema change or workload shift may require updating the configuration. The DAGs themselves become a maintenance burden — and the engineer who wrote them is often no longer on the team.

At 500 tables, the Airflow scheduler itself becomes a bottleneck. DAG parsing slows. The metadata database bloats. Concurrent task execution saturates the worker pool. Teams add more Airflow infrastructure to maintain more maintenance DAGs — a recursion that data platform engineers recognize but rarely admit on conference talks.

LakeOps replaces per-table configuration with a hierarchical policy model: set a compaction policy at the catalog level, override at the namespace level for streaming workloads, and add table-level exceptions where needed. Three policies cover 500 tables. Adding a new table requires zero configuration — it inherits the policy from its namespace. Dropping a table removes it automatically. This is the managed lakehouse governance model that scales linearly with organizational complexity, not table count.

LakeOps Tables — every table classified by health status, size, and record count across the lake
Every table in the lake classified as Healthy, Warning, or Critical — with size, record count, and degradation signals. No per-table DAG needed. The control plane manages all of them through hierarchical policies.
LakeOps Policies — catalog-wide compaction, orphan cleanup, snapshot expiry, and manifest rewrite schedules
Three policies replace 500 DAG configurations. A catalog-level compaction policy applies to every table beneath it. Namespace overrides handle streaming workloads. Table exceptions handle edge cases. Adding a new table requires zero configuration.

Pitfall 3: JVM overhead compounds at scale

Each Spark maintenance job carries JVM startup time (2–5 minutes for cluster provisioning and executor allocation), garbage collection pauses, and per-executor memory overhead. For a single table, this is negligible. Across 100 tables, the aggregate startup time alone is 3–8 hours per maintenance cycle. The compute bill for maintenance can rival the compute bill for production queries.

On tables above 1 TB with complex delete patterns (merge-on-read workloads), Spark compaction routinely OOMs. The fix is cluster resizing — larger executors, more memory, higher cost — or job splitting (compact 10 partitions at a time), which adds more DAG complexity. Tables that should take 4 minutes to compact take 25 minutes because the JVM overhead dominates the actual I/O work.

Rust-powered compaction — small Parquet files merged into optimally sized files without JVM overhead
LakeOps compaction runs on a purpose-built Rust engine — no JVM startup, no garbage collection pauses, no cluster provisioning. In production benchmarks, binpack completes in 221 seconds versus 1,612 for Spark on the same 200 GB table.

A Rust-based compaction engine eliminates JVM overhead entirely. LakeOps runs compaction on Apache DataFusion — no executor provisioning, no GC pauses, no cluster scaling. Production benchmarks show 95% faster completion times versus equivalent Spark jobs. Tables that cause Spark to OOM are handled without special configuration. The maintenance compute cost drops from the range of $30–50/TB (Spark clusters) to $3–5/TB (Rust engine). For a 100 TB lakehouse, that is the difference between $3,000/month and $300/month in maintenance compute alone. See compaction at scale for the full benchmark methodology.

Production benchmarks — 95% faster compaction, 12x query acceleration, 80% cost reduction
Production benchmarks across streaming, batch, and delete-heavy workloads — 95% faster compaction versus Spark, 12x query acceleration from optimized file layout, and 80% reduction in maintenance compute cost. The Rust engine handles tables that OOM Spark without configuration changes.

Pitfall 4: No cross-operation coordination

Even a well-structured Airflow DAG with expire >> orphans >> compact >> manifests enforces the correct sequence within a single table. It does not coordinate across tables, across catalogs, or with production workloads.

Compaction on Table A may run concurrently with a streaming writer on the same Spark cluster, causing commit conflicts and OCC retries. Orphan cleanup on Table B may fire before snapshot expiration on Table C has finished — and if Table B and Table C share storage prefixes, the cleanup may incorrectly identify in-progress files as orphans. A manifest rewrite that starts at 2:15 AM may collide with a compaction job that is still running from the 2:00 AM schedule.

These coordination failures are intermittent, which makes them hard to diagnose. The compaction job succeeds 28 out of 30 days. On the two days it fails, the retry logic either catches it (masking the root cause) or gives up (leaving the table uncompacted until the next cycle). Over months, intermittent failures accumulate into persistent drift: tables that are nominally maintained but structurally degraded.

A control plane that owns the entire maintenance lifecycle solves this structurally. LakeOps sequences operations per table in the correct order, coordinates across tables to avoid cluster contention, uses partition-level awareness to skip hot partitions with active writers, and implements OCC-aware retry logic that reapplies operations to the specific partitions that conflicted — not the entire table. Maintenance and production writes never collide because the control plane sees both. For deeper context on conflict patterns, see Iceberg commit conflicts: causes, prevention, and recovery.

LakeOps Events — sequenced compaction, snapshot expiry, and manifest rewrite operations with impact metrics
Every operation is sequenced, logged, and measured — expire snapshots, then compact data files, then rewrite manifests. Each entry shows file reduction, duration, and storage impact. No ordering mistakes, no concurrent conflicts.

Pitfall 5: Observability is an afterthought

Airflow provides DAG-level observability: task status, duration, retry count, and logs. It does not provide table-level observability: how many small files does each table have? Which tables are degraded? How many orphan files are accumulating? What is the query-time impact of the current file layout?

Without table-level health metrics, maintenance runs blind. The DAG succeeded — but did it actually improve the table? A compaction job that rewrites 10 files into 8 files succeeded by Airflow's definition, but it did not solve the small-file problem. A snapshot expiration that removed 5 snapshots succeeded, but the table still has 4,000 snapshots because the retention window is misconfigured.

LakeOps closes this gap with continuous health scoring: every table classified as Healthy, Warning, or Critical based on structural metrics — small-file ratio, snapshot depth, manifest fragmentation, delete-file accumulation, and partition skew. Tracked per table, trended over time, and surfaced as actionable insights ranked by severity. When a table degrades, the control plane triggers maintenance immediately. When a table is already healthy, it does not waste compute running unnecessary operations. This closed-loop model — observe health, trigger maintenance, measure impact, adapt — is what makes the difference between maintenance as a chore and maintenance as infrastructure.

LakeOps Adaptive Maintenance — compaction, snapshot expiry, manifest rewrite, and orphan cleanup triggered by health signals
Adaptive Maintenance drives the closed loop — health signals trigger compaction, snapshot expiry, manifest rewrite, and orphan cleanup automatically. When the table improves, operations pause. When it degrades again, they resume. No human in the loop.
LakeOps Insights — table health issues ranked by severity with specific degradation signals
Insights ranked by severity — small-file proliferation, excessive snapshots, manifest bloat, partition skew. Each issue identifies the table, the signal, and the recommended action. This is the view that replaces checking 200 Airflow DAG statuses and hoping the tables are actually healthy.

The five pitfalls are structural

These pitfalls are not bugs in your DAGs. They are structural limitations of using a workflow orchestrator for a control plane problem.

Airflow was designed to orchestrate data pipelines — extract, transform, load — with static dependency graphs and time-based schedules. Table maintenance requires dynamic, state-driven orchestration: monitoring table health continuously, triggering operations based on structural thresholds, coordinating across tables and engines, adapting to workload changes in real time, and providing table-level observability that feeds back into the next decision.

You can add health checks to your DAGs. You can parameterize schedules. You can build custom operators that inspect table metadata before deciding whether to compact. Teams do this — and they end up with 2,000 lines of Python glue code, a custom metadata store, a health-scoring system, a conflict-avoidance layer, and an alerting pipeline. At that point, you have built a control plane inside Airflow. It is harder to maintain than the original DAGs, harder to debug, and harder to hand off to the next engineer.

What the control plane adds beyond fixing pitfalls

You have seen the individual pieces throughout — adaptive triggers for Pitfall 1, hierarchical policies for Pitfall 2, the Rust engine for Pitfall 3, partition-level coordination for Pitfall 4, and health scoring for Pitfall 5. Two additional capabilities have no DAG equivalent at all:

Query-aware layout optimization. Beyond merging small files to a target size, LakeOps analyzes cross-engine query telemetry — which columns appear in WHERE clauses across Trino, Spark, Athena, Snowflake, and DuckDB — and sorts data files around those columns. Statistical pruning skips 90%+ of files before any data is read. Layout simulations let you test sort strategies against real query patterns before committing to a rewrite — projected scan reduction, file skip rate, and query planning impact, all without writing a single file.

LakeOps Layout Simulations — query-aware sort strategies replayed against production SQL
Layout simulations replay your production SQL against candidate sort strategies — showing the impact on file pruning and scan width before you commit to a rewrite. The control plane determines which columns matter based on actual cross-engine telemetry.

Lake-wide operations monitoring. Beyond per-table health, the Monitoring view shows operations coverage, readiness state, and optimization timeline across every catalog and namespace. You see what ran, what is pending, and what needs attention — in a single view that replaces checking hundreds of Airflow DAG statuses.

LakeOps Monitoring — operations coverage, readiness, and optimization timeline across the lake
Operations coverage, readiness state, and optimization timeline — across every catalog and namespace. One view replaces hundreds of Airflow DAG statuses.
Four steps from zero to autonomous maintenance — connect catalogs, choose execution mode, operations run, observability and governance
Connect your existing catalogs → choose adaptive or scheduled mode → operations run autonomously → full observability and governance. Most teams go from zero to autonomous maintenance within 15 minutes.
Watch demoYouTube ↗
LakeOps product walkthrough — catalog connection, table health analysis, and autonomous optimization for Iceberg tables.

Migration path: Airflow to autonomous

Replacing Airflow maintenance does not require replacing Airflow. Most teams run LakeOps alongside their existing orchestration — Airflow continues to schedule ETL, ingestion, and transformation pipelines. Maintenance shifts to the control plane.

Connected catalogs — AWS Glue, DynamoDB-backed REST, S3 Tables syncing automatically
Step 1 takes minutes — connect your existing Iceberg catalogs (Glue, REST/Polaris, S3 Tables). Every table in the catalog is discovered and scored automatically.

The migration is incremental:

  1. 1.Connect catalogs. Point LakeOps at your existing Iceberg catalogs (Glue, REST, S3 Tables). Table discovery is automatic — every table in the catalog is visible within minutes.
  2. 2.Review health. The dashboard shows which tables are degraded and why. Most teams discover tables they did not know were unhealthy — the silent degradation that Airflow DAGs cannot surface.
  3. 3.Enable adaptive maintenance. Start with a single namespace or catalog. Adaptive Maintenance bundles compaction, snapshot expiry, manifest rewrites, and orphan cleanup into one policy that triggers on health signals. See the LakeOps quick start for the full walkthrough.
  4. 4.Retire DAGs gradually. As each namespace moves to autonomous maintenance, disable the corresponding Airflow DAGs. Keep Airflow for pipeline orchestration — it is excellent at that. Remove the maintenance burden it was never designed for.

For teams that prefer more control, LakeOps also supports scheduled and manual modes — cron-based execution, fixed intervals, or one-off runs. You can start with scheduled policies that mirror your current Airflow cadence, validate the results, then switch to adaptive when you are comfortable.

The bottom line

Airflow is a great workflow orchestrator. It is not a lakehouse maintenance system. The five pitfalls — fixed schedules, per-table DAGs, JVM overhead, missing coordination, and blind-spot observability — are not operational failures. They are architectural mismatches between a tool designed for pipeline orchestration and a problem that requires continuous, state-driven, conflict-aware, query-informed table management.

Production results — 95% faster compaction, 12x query acceleration, 80% cost reduction
The outcome across production lakehouses — 95% faster compaction, 12x query acceleration, 80% reduction in maintenance compute cost. These are the numbers when Airflow maintenance is replaced by a purpose-built control plane.

At 10 tables, the mismatch is invisible. At 50 tables, it is annoying. At 200 tables with mixed workloads across multiple engines, it is the dominant source of performance degradation, wasted compute, and engineering toil in your lakehouse.

The alternative is not more Airflow. It is a dedicated control plane for your data lakehouse — one that observes every table continuously, triggers maintenance when the table needs it, sequences operations correctly, avoids conflicts with production writes, sorts data for how you actually query, and gives you lake-wide observability that Airflow's task logs never will.

Your Airflow DAGs got you this far. The lakehouse outgrew them. That is not a failure — it is a signal that the maintenance problem has matured into a systems problem, and systems problems need purpose-built solutions.

Tags

Data PlatformsApache AirflowData LakehouseApache IcebergCompactionLakeOpsLakehouse MaintenanceIceberg Maintenance

Related articles

Found this useful? Share it with your team.