Home Resume
Home Blog AWS Architecture Series #31 — Iceberg on S3: Why Table Formats Arrived…
AWS Architecture AWS Architecture Series

AWS Architecture Series #31 — Iceberg on S3: Why Table Formats Arrived

A table format is adopted as a correctness fix — atomic commits, real deletes, schema evolution that means something — and it delivers all of it. What arrives with it is an operating cost nobody budgeted: a table whose file list is now a data structure that must be pruned on a schedule. AWS offers three different owners for that schedule, with different defaults, expressed in different units, and every one of their failure modes is a quiet deletion or a silently suspended job.

Verified against current vendor documentation on 24 August 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

Business Challenge

The previous post ended on a table with four schemas and no arbiter, and that is the honest summary of schema-on-read: the catalog describes files, the description is applied at query time, and when the description and the files disagree the reader decides. Table formats are what happened when a decade of that argument was lost. Apache Iceberg does not add a feature to that model — it moves the table's definition out of the catalog and into the data, so that the list of files belonging to a table is a written, versioned artefact rather than whatever a LIST against a prefix returned this morning.

That change lands cleanly. Athena describes it in one sentence: each Iceberg table maintains a versioned manifest of the S3 objects that it contains. From that single property everything the format is known for follows. A commit is atomic because it swaps one pointer. A delete is real because the row's absence is recorded rather than inferred. Time travel exists because the old manifest was never overwritten. Schema evolution means something because columns have identity independent of their name and position.

The part that does not land cleanly is what those manifests cost to keep. A Hive table on S3 has no maintenance: objects arrive, the crawler notices, and nothing accumulates that a reader must traverse. An Iceberg table accumulates snapshots, manifests, metadata files and — on AWS in particular — delete files, all of which a query engine reads before it reads any data. Left alone, an Iceberg table gets slower and more expensive in a way a Hive table does not.

So the format solves a correctness problem and creates a scheduling problem. AWS answers the scheduling problem three separate times: manually through Athena's OPTIMIZE and VACUUM, automatically through AWS Glue Data Catalog table optimizers, and automatically-and-not-yours through S3 Tables. The three have different defaults, expressed in different units, with different things they refuse to do. Choosing between them is the actual architecture decision, and it is made after the interesting part — picking the format — is already over and everyone has moved on.

The distinction that matters throughout

Schema-on-read fails by changing an answer. A table format fails by deleting a file or by stopping. Both are quiet, but they are quiet in opposite directions: the first returns a plausible result set from a table nobody has broken, the second returns a correct result set from a table that is degrading, or that has lost the history you were relying on. The monitoring that catches one does not catch the other.

Architecture

The design question is not "should we use Iceberg" — for a mutable analytic table on S3 that is close to settled. It is "which component owns this table's file list, and what is it allowed to delete".

Diagram: how a Hive table on S3 resolves a query by listing prefixes and reading catalog partitions compared with how an Iceberg table resolves one through a catalog pointer, metadata file, snapshot, manifest list and manifests, followed by the three AWS owners of Iceberg table maintenance with their default retention and compaction thresholds, and the ways each of them deletes files or stops working without raising an error

What actually changed: the file list stopped being a directory listing

In the Hive model a table is a prefix plus a set of partitions in the catalog, and the files in a partition are whatever is under its location. The catalog does not know how many objects exist, which is why the scale numbers do not line up: Athena will query a Glue table with 10 million partitions, but it cannot read more than 1 million partitions in a single scan. That is a factor of ten between what the catalog is willing to hold and what a query can traverse, and the gap is bridged by hoping the predicates prune well.

Iceberg replaces the enumeration with a tree. A pointer in the catalog names the current metadata file; the metadata file names a snapshot; the snapshot names a manifest list; the manifest list names manifests; the manifests name data files, each with per-column bounds. A query prunes at the manifest level using those bounds, so the work is proportional to what the predicate matches rather than to how the data is laid out. This is why hidden partitioning is not a marketing phrase: the partition transform is recorded in the table, so a query filtering on a timestamp gets partition pruning without the analyst knowing a partition column exists, and the transform can be changed later without rewriting the query or the old data.

Two consequences that are easy to miss. First, the catalog's role shrinks to holding one pointer and swapping it atomically — which is why Athena is explicit that it supports AWS Glue optimistic locking only, and warns that modifying an Iceberg table through any other lock implementation "will cause potential data loss and break transactions". The atomicity everything else rests on is a property of that one swap. Second, the metadata is now a read cost. Every query walks the tree before it touches data, and the tree grows with every commit until something prunes it.

Row-level change is real, and the write mode is not yours to choose

UPDATE, DELETE and MERGE INTO in Athena follow the Iceberg v2 positional delete specification and enforce snapshot isolation. Athena only creates and operates on v2 tables, and supports Iceberg 1.4.2 with Parquet, ORC and Avro data files. That is a genuine capability and it is the reason most teams arrive here: erasure requests, late-arriving corrections and CDC upserts stop being a full-partition rewrite.

What is worth knowing before designing around it is that Athena implements exactly one of the two Iceberg write modes. It always uses merge-on-read. The documentation is unusually direct about the consequence:

Athena SQL does not currently support the copy-on-write approach. UPDATE, MERGE INTO and DELETE FROM always use merge-on-read with positional deletes, regardless of specified table properties. If you have set write.update.mode, write.merge.mode or write.delete.mode to copy-on-write, the queries will not fail — Athena ignores those properties and keeps using merge-on-read.

Read that as an operator. A table property set deliberately, reviewed in a pull request, and present in the table's metadata is inert, and nothing announces it. The effect is not cosmetic: merge-on-read defers the cost of a delete to every subsequent read, and the deferred cost only comes back down when compaction merges the delete files into data files. So on Athena, compaction is not a storage-cost optimisation that can be postponed — it is the second half of every DELETE that has been run.

Which makes the compaction thresholds worth reading as policy rather than as tuning knobs. optimize_rewrite_delete_file_threshold defaults to 2: a data file with fewer than two associated delete files is not rewritten, deliberately, to let delete files accumulate and make the eventual rewrite worth its cost. optimize_rewrite_data_file_threshold defaults to 5, and both are capped below 50. Raising them buys cheaper compaction and slower reads, in that order.

-- Compaction is charged by data scanned, and only partition columns
-- may appear in the predicate. Scope it, or the table is rewritten.
OPTIMIZE sales.orders REWRITE DATA USING BIN_PACK
  WHERE event_date >= DATE '2026-08-01';

-- Snapshot expiry and orphan removal. Set the retention first --
-- the default is five days, and this is what ends time travel.
ALTER TABLE sales.orders SET TBLPROPERTIES (
  'vacuum_max_snapshot_age_seconds' = '2592000',   -- 30 days
  'vacuum_min_snapshots_to_keep'    = '10'
);
VACUUM sales.orders;

Three owners of maintenance, three sets of defaults

The same three jobs — compact files, expire snapshots, remove unreferenced files — are offered by three AWS surfaces, and picking one is picking a set of defaults that will probably never be revisited.

Athena OPTIMIZE / VACUUMGlue Data Catalog optimizersS3 Tables
RunsWhen you run itContinuously, when thresholds are exceededContinuously, managed by S3
Compaction triggerFewer than 5 candidate data files, skipMore than 100 files, each below 75% of targetNot exposed
Target file sizeTable property, per tablewrite.target-file-size-bytes, default 512 MB512 MB default, 64–512 MB range
Snapshot retention default432,000 seconds, minimum 1 snapshotSet when the optimizer is created120 hours, minimum 1 snapshot
Metadata file retention100 filesHandled by the optimizerNot exposed
File formatsParquet, ORC, AvroParquet onlyParquet, Avro, ORC
Statistics for the planneruse_iceberg_statistics, opt inGlue column statisticsOn by default
Fails byNot being runAuto-suspending after 4 consecutive failuresFailing the whole table's snapshot job

The row worth staring at is snapshot retention. Athena's default is 432,000 seconds and S3 Tables' default is 120 hours. Those are the same number — five days — written in two units, on two surfaces, in two consoles. It is a sensible default for a storage bill and a poor one for the reason most people say they want a table format. If the recovery story is "we can roll the table back", the untouched version of that story lasts until Friday.

Time travel is a retention policy wearing a feature's clothes

The syntax is deliberately unremarkable: FOR TIMESTAMP AS OF for a point in time, FOR VERSION AS OF with a bigint snapshot ID for a specific commit. Both replaced the FOR SYSTEM_TIME AS OF and FOR SYSTEM_VERSION AS OF forms from Athena engine version 2, which is worth knowing when inheriting a query that will not parse.

-- What the table said before this morning's load
SELECT * FROM sales.orders FOR TIMESTAMP AS OF (current_timestamp - interval '1' day);

-- Diff a snapshot against the current state
SELECT c.order_id
FROM sales.orders c
FULL JOIN sales.orders FOR VERSION AS OF 949530903748831860 p
  ON c.order_id = p.order_id
WHERE c.order_id IS NULL OR p.order_id IS NULL;

The warning attached to VACUUM is the whole story: run a snapshot expiration and you can no longer time travel to expired snapshots. There is no separate archive, no recycle bin and no soft delete. Retention is the feature, and the two automated owners both ship it at five days.

One asymmetry that catches people: vacuum_min_snapshots_to_keep takes precedence over the age. If the minimum remaining snapshots are older than vacuum_max_snapshot_age_seconds, they are kept and the age is ignored. So a table that has not been written to in a month still has its last snapshot, and a table written to every minute has five days. The retention that results depends on the write rate, which is not how anybody reads that setting.

Every failure mode here is a deletion or a stop

This is the part that repays reading the limitations pages rather than the launch posts. None of the following raises an error at the time it matters.

  • Two tables, one prefix. When multiple Glue Data Catalog tables share the same S3 location and have optimizers enabled, one table's snapshot retention or orphan file deletion may delete files still referenced by the other. This is not a corner case in practice — a table and its _backup, or two catalog entries pointed at one curated path, gets there quickly.
  • An S3 lifecycle rule over the table path. AWS states it plainly: lifecycle expiration rules applying to Iceberg table storage locations can delete manifest and data files still referenced by active snapshots. The standard "expire everything older than 90 days" rule, applied at the bucket level for cost hygiene, is a data-loss mechanism on an Iceberg prefix.
  • Iceberg-native retention on an S3 table. Setting history.expire.max-snapshot-age-ms or history.expire.min-snapshots-to-keep as a table property does not configure S3 Tables snapshot management — it makes it fail for the entire table, regardless of the value. Any user-defined tag or branch does the same. The table keeps working, nothing expires, metadata grows, and the only signal is GetTableMaintenanceJobStatus.
  • Four failures and compaction stops. Glue automatically suspends a compaction optimizer after four consecutive failures, to avoid burning compute. Correct behaviour, and it means an optimizer enabled six months ago is not evidence that compaction ran last night.
  • The million-file ceiling. Glue snapshot retention and orphan file deletion delete at most 1,000,000 files per run. Anything eligible beyond that stays in table storage as orphan files — so the job reports success while the backlog it was meant to clear persists.
  • Orphan deletion has an epoch. The Glue orphan file deletion optimizer only removes files created after the optimizer's own creation date. Files created before or on that date are never deleted. Enabling it to clean up years of accumulated debris does precisely nothing to the debris.
  • VACUUM without delete permission. If the query execution role lacks s3:DeleteObject, the VACUUM query succeeds and no files are removed. A green query history and a growing bill.

Where S3 Tables changes the shape of the problem

S3 Tables is a bucket type whose subresource is a table, and the argument for it is not only that maintenance becomes somebody else's job. It is a request-rate argument. A general purpose bucket gives at least 3,500 PUT, COPY, POST or DELETE requests, or 5,500 GET and HEAD requests, per second per partitioned prefix, with no limit on the number of prefixes — a fine model when the key layout spreads naturally, and a poor one for an Iceberg table whose commits concentrate on a metadata path. AWS states S3 Tables deliver up to 10x higher transactions per second than Iceberg tables in general purpose buckets, and that is the number to weigh, rather than the maintenance automation.

The defaults are opinionated in a useful direction. Compaction targets 512 MB, configurable between 64 MB and 512 MB, with a strategy of auto that applies sort compaction to a table with a defined sort order and binpack to one without. S3 Tables applies the Parquet row-group default of 128 MB, so a compacted file at the default target holds four row groups — the unit a scan actually skips. Iceberg statistics and Parquet column indexes are on by default here and opt-in everywhere else, through use_iceberg_statistics and use_iceberg_parquet_column_index, which is a real difference in query plans and not a footnote.

The constraint to design against is the quota shape: 10 table buckets per Region per account, each holding up to 10,000 namespaces and 10,000 tables, all adjustable only by contacting Support. Ten is a small number if the instinct is one bucket per team or per environment. Namespaces are the intended axis of separation, and they are cheap; buckets are not.

The parts that are still sharp

Two limits worth surfacing before they are found during an audit. Athena does not support DDL on Iceberg tables registered with Lake Formation, and Lake Formation cannot manage permissions for VACUUM, MERGE, UPDATE or OPTIMIZE on Iceberg tables at all — it governs read access. If the governance model assumes Lake Formation mediates everything, maintenance operations sit outside it and need IAM.

And a data-fidelity one: Iceberg supports microsecond precision on timestamps; Athena supports milliseconds, on both reads and writes, and retains only milliseconds for time columns rewritten during manual compaction. For most analytics that is invisible. For event ordering, idempotency keyed on an event timestamp, or anything reconciling against a source system that records microseconds, it is a silent truncation performed by a maintenance job.

Why This Architecture Holds Up

The previous post argued that schema-on-read fails by changing answers, and that the failure is invisible in every job metric. Table formats fix that class of failure convincingly: the schema is in the data, column identity survives a rename, a commit either happened or did not, and the state of the table on any past day is a query rather than a restore. If the problem is correctness, the argument is over.

What the format does is trade a correctness risk for an operational one, and operational risks are only better if they are actually operated. An Iceberg table with no maintenance owner does not return wrong answers. It returns right answers, slowly, at rising cost, with a history that quietly stopped being retained — and the first symptom is a bill or a failed recovery, both of which arrive long after the decision that caused them.

That is why the monitoring has to change with the format. On a Hive table the useful alarms were about drift: table count per database, partition count per table. On an Iceberg table they are about maintenance liveness. The highest-value signal is the maintenance job's own status, because every automated owner has a documented way of stopping while the table carries on serving: a Glue optimizer auto-suspends after four failures, and S3 Tables snapshot management fails for a whole table if somebody adds a branch or sets an Iceberg retention property. Alarm on job status and on metadata file count per table, and the two failures that produce no error become the two seen first.

The second-order point is about who is allowed to delete. A Hive table on S3 tolerated a bucket-wide lifecycle rule, a shared prefix and a tidy-up script, because nothing but the objects themselves mattered. An Iceberg table's correctness depends on files that look unreferenced and are not, so every one of those habits becomes a way to lose data. The rule that follows is blunt: one table, one prefix, one deleter.

Key Architecture Decisions

DecisionTake thisBecause
Who owns maintenance Exactly one owner per table, recorded somewhere a human reads The three surfaces overlap, and two of them deleting against one table is how files referenced by a live snapshot disappear
Snapshot retention Set it explicitly, on day one, to the recovery window actually being promised Both automated defaults are five days — 432,000 seconds on Athena, 120 hours on S3 Tables — and expiry is irreversible
Retention on an S3 table PutTableMaintenanceConfiguration, never history.expire.* The Iceberg-native property does not configure snapshot management; it makes it fail for the whole table, silently
Prefix layout One table per S3 location, never shared, never nested under another table Shared locations let one table's optimizer delete another's live files, and break orphan detection in both directions
Lifecycle rules Exclude every Iceberg table path from bucket-level expiration Lifecycle expiry deletes manifest and data files that are still referenced by active snapshots
Deletes on Athena Budget compaction as part of the delete, not as cleanup Athena is merge-on-read only and ignores copy-on-write table properties, so read cost stays elevated until OPTIMIZE runs
OPTIMIZE scope Always a partition predicate; never a bare OPTIMIZE on a large table It is charged by data scanned, and any file containing one matching row is rewritten
Glue managed compaction Only for Parquet tables, and re-check that it is still enabled ORC and Avro are not supported, and four consecutive failures suspend the optimizer without further notice
S3 Tables bucket count Separate by namespace, not by bucket 10 table buckets per Region per account, increasable only through Support; namespaces go to 10,000
Detection Alarm on maintenance job status and metadata file count per table A stopped optimizer and a failed snapshot job both leave a table answering every query correctly

Closing Thought

Table formats arrived because the alternative had run out of room. Schema-on-read made a lake cheap to fill and impossible to guarantee, and every mechanism built on top of it — crawler policies, partition inheritance, format-specific access rules — was reconciliation after the fact. Iceberg's answer is not cleverer reconciliation. It is to write the table down.

What that buys is a real transaction boundary, and what it costs is that a table is now a living data structure with a maintenance schedule. The AWS surfaces around it are good and getting better — managed compaction, S3 Tables' automated maintenance, and the request rates that come with a purpose-built bucket type are genuine improvements over rolling it yourself. But every one of them defaults to five days of history, and every one of them has a documented way of stopping without saying so.

Which turns the interesting question away from the format and onto ownership. The format has made the table honest. Whether the estate stays healthy depends on something much duller: knowing, per table, which component is allowed to delete a file, and having an alarm on whether it still is.

Next in this series

#32 — Redshift or Athena: when a warehouse earns its keep. Two posts of catalog and table format have been about making files behave like a table. The next one asks the opposite question: at what point does the query engine stop being the cheap part, and what does a warehouse give that a well-maintained Iceberg table on S3 does not.

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent