apache/iceberg · error · MaxCommittedCheckpointMismatchException

Table already contains staged changes.

Error message

Table already contains staged changes.

What it means

The committer's validation hook checks that the checkpoint being committed is not older than the maximum checkpoint already committed for this flinkJobId/operatorId found in the base snapshots. If maxCommittedCheckpointId >= stagedCheckpointId, the staged changes are stale (already committed or superseded), so it throws MaxCommittedCheckpointMismatchException with this message rather than double-committing. The given SOURCE shows the equivalent validate() path that guards this invariant.

Source

Thrown at flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommitter.java:358

  private static class MaxCommittedCheckpointIdValidator implements SnapshotAncestryValidator {
    private final long stagedCheckpointId;
    private final String flinkJobId;
    private final String flinkOperatorId;

    private MaxCommittedCheckpointIdValidator(
        long stagedCheckpointId, String flinkJobId, String flinkOperatorId) {
      this.stagedCheckpointId = stagedCheckpointId;
      this.flinkJobId = flinkJobId;
      this.flinkOperatorId = flinkOperatorId;
    }

    @Override
    public boolean validate(Iterable<Snapshot> baseSnapshots) {
      long maxCommittedCheckpointId =
          getMaxCommittedCheckpointId(baseSnapshots, flinkJobId, flinkOperatorId);
      if (maxCommittedCheckpointId >= stagedCheckpointId) {
        throw new MaxCommittedCheckpointMismatchException();
      }

      return true;
    }
  }

  @VisibleForTesting
  void commitOperation(
      Table table,
      String branch,
      SnapshotUpdate<?> operation,
      CommitSummary summary,
      String description,
      String newFlinkJobId,
      String operatorId,
      long checkpointId) {

    LOG.info(

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Treat as benign if the data is already committed: the commit is idempotent; verify the latest snapshot and skip/retry the checkpoint
  2. Restore from a savepoint/checkpoint that matches the table's committed state
  3. Ensure flinkJobId is unique per deployment so checkpoint bookkeeping isn't shared between jobs
Defensive patterns

Strategy: retry

Validate before calling

// Before committing, check the table's latest committed checkpoint for this job:
long maxCommitted = getMaxCommittedCheckpointId(table.snapshots(), flinkJobId, operatorId);
if (maxCommitted >= stagedCheckpointId) { /* skip: already committed */ }

Try / catch

try {
  committer.commit(committables);
} catch (MaxCommittedCheckpointMismatchException e) {
  // data already committed; verify latest snapshot and continue with next checkpoint
}

Prevention

When it happens

Trigger: Committing a DynamicCommittable whose stagedCheckpointId is <= the highest checkpoint id already recorded in table snapshots for the same flink job/operator, e.g. on retry after a commit actually succeeded but the response was lost, or when restoring from a savepoint older than the table state.

Common situations: Job restarted from a savepoint while the table already contains commits from a later checkpoint; Flink's checkpoint retry semantics racing with a slow but successful Iceberg commit; running two jobs with the same flinkJobId against one table.

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/c2d931ab179d0857. Report an issue: GitHub.