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
- Treat as benign if the data is already committed: the commit is idempotent; verify the latest snapshot and skip/retry the checkpoint
- Restore from a savepoint/checkpoint that matches the table's committed state
- 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
- Restore jobs from savepoints consistent with the table's committed state
- Ensure each deployment uses a unique flinkJobId
- Rely on the sink's idempotent checkpoint bookkeeping rather than manual commits
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
- Table already contains staged changes.
- Unrecognized version or corrupt state: <version>
- Unknown serialize version: ${version}
- Unrecognized version or corrupt state: ${version}
- Failed to close equality delta writer
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/c2d931ab179d0857.
Report an issue: GitHub.