apache/iceberg · error · CommitFailedException
Cannot commit: stale table metadata
Error message
Cannot commit: stale table metadata
What it means
commit(base, metadata) rejects a commit when the supplied base metadata is non-null but differs from the currently cached metadata (by reference), meaning the table changed since the caller read it. This optimistic-concurrency check prevents silently overwriting intermediate updates; the caller must refresh and reapply its changes.
Source
Thrown at core/src/main/java/org/apache/iceberg/BaseMetastoreTableOperations.java:113
currentMetadata = null;
currentMetadataLocation = null;
version = -1;
throw e;
}
return current();
}
protected void doRefresh() {
throw new UnsupportedOperationException("Not implemented: doRefresh");
}
@Override
public void commit(TableMetadata base, TableMetadata metadata) {
// if the metadata is already out of date, reject it
if (base != current()) {
if (base != null) {
throw new CommitFailedException("Cannot commit: stale table metadata");
} else {
// when current is non-null, the table exists. but when base is null, the commit is trying
// to create the table
throw new AlreadyExistsException("Table already exists: %s", tableName());
}
}
// if the metadata is not changed, return early
if (base == metadata) {
LOG.info("Nothing to commit.");
return;
}
long start = System.currentTimeMillis();
doCommit(base, metadata);
CatalogUtil.deleteRemovedMetadataFiles(io(), base, metadata);
requestRefresh();
LOG.info(View on GitHub (pinned to 86d9c8fc54)
Solutions
- Refresh operations (ops.refresh()) and rebuild base from the current metadata, then reapply changes
- Use the table's Transaction API, which manages base/current tracking for you
- Catch CommitFailedException in the commit retry loop and restart with fresh metadata
- Coordinate writers so only one process commits at a time
Example fix
// before
ops.commit(staleBase, newMetadata); // CommitFailedException
// after
ops.refresh();
TableMetadata freshBase = ops.current();
TableMetadata rebased = TableMetadata.buildFrom(freshBase)
.setCurrentSchema(newSchema, newSchema.highestFieldId())
.build();
ops.commit(freshBase, rebased); Defensive patterns
Strategy: retry
Validate before calling
ops.refresh(); // ensure cached base matches current before building the commit
Try / catch
boolean done = Tasks.foreach(...).retry(3).onlyRetryOn(CommitFailedException.class).run(...) — or:
try { ops.commit(base, metadata); }
catch (CommitFailedException e) { ops.refresh(); base = ops.current(); /* reapply and retry */ } Prevention
- Re-read base metadata immediately before commit
- Use the Table/Transaction APIs instead of raw commit
- Keep commit windows short; avoid long-lived Table references across external changes
When it happens
Trigger: A long-running transaction or retry loop passes a TableMetadata base that no longer matches current(), e.g. after another commit landed or after refresh invalidated the cached base.
Common situations: Two jobs committing to the same table concurrently; a retry loop reusing a stale base across attempts; holding a Table reference across external catalog updates (schema updates by another engine) before committing.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot commit: stale view metadata
- Requirement failed: table already exists
- Requirement failed: %s %s was created concurrently
- Requirement failed: %s %s has changed: expected id %s != %s
- Requirement failed: branch or tag %s is missing, expected %s
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/a72e28313062576c.
Report an issue: GitHub.