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

  1. Refresh operations (ops.refresh()) and rebuild base from the current metadata, then reapply changes
  2. Use the table's Transaction API, which manages base/current tracking for you
  3. Catch CommitFailedException in the commit retry loop and restart with fresh metadata
  4. 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

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


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/a72e28313062576c. Report an issue: GitHub.