aeron-io/aeron · error · ClusterException

incompatible app version

Error message

incompatible app version: ${SemanticVersion.toString(ctx.appVersion())} snapshot=${SemanticVersion.toString(appVersion)}

What it means

Thrown as ClusterException when the appVersion stored in the loaded snapshot is not compatible (per the configured appVersionValidator) with the container's configured appVersion. It prevents loading snapshots written by an incompatible application version, guarding state format changes across releases.

Solutions

  1. Update the configured appVersion (aeron.cluster.app.version) or the appVersionValidator to accept the snapshot's version
  2. Write a fresh snapshot with the new application version before retiring the old one (take a snapshot before upgrade)
  3. Restore from a snapshot produced by a compatible version, or replay the log from an earlier point
  4. Implement SemanticVersion-based compatibility (same major, snapshot minor <= current) in your validator

Example fix

// before
container.context().appVersion(0x00010000); // 1.0, snapshot is 2.0
// after
container.context().appVersion(0x00020000)
         .appVersionValidator((configVersion, snapshotVersion) ->
             SemanticVersion.major(snapshotVersion) <= SemanticVersion.major(configVersion));
Defensive patterns

Strategy: validation

Validate before calling

int snapshotVersion = snapshotLoader.appVersion();
if (!ctx.appVersionValidator().isVersionCompatible(ctx.appVersion(), snapshotVersion)) {
    throw new IllegalStateException("snapshot appVersion " + snapshotVersion + " incompatible with " + ctx.appVersion());
}

Try / catch

try { loadSnapshot(in); } catch (ClusterException ex) { /* restore from a compatible snapshot instead */ }

Prevention

When it happens

Trigger: Restoring a cluster from a snapshot whose recorded appVersion fails ctx.appVersionValidator().isVersionCompatible(ctx.appVersion(), snapshotVersion); deploying a new release against old snapshots; changing the default appVersion without a compatible validator.

Common situations: Rolling upgrades/rollbacks where the old snapshot predates a state format change; forgetting to bump or set aeron.cluster.app.version; custom validator rejecting a minor-version mismatch.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of aeron-io/aeron@6d60124e15 (2026-09-12). Data as JSON: /api/errors/858f32ac871f0c42. Report an issue: GitHub.

Appendix: source

Thrown at aeron-cluster/src/main/java/io/aeron/cluster/service/ClusteredServiceAgent.java:980

                break;
            }

            if (0 == fragments)
            {
                archive.checkForErrorResponse();
                if (image.isClosed())
                {
                    throw new ClusterException("snapshot ended unexpectedly: " + image);
                }
            }

            idle(fragments);
        }

        final int appVersion = snapshotLoader.appVersion();
        if (!ctx.appVersionValidator().isVersionCompatible(ctx.appVersion(), appVersion))
        {
            throw new ClusterException(
                "incompatible app version: " + SemanticVersion.toString(ctx.appVersion()) +
                " snapshot=" + SemanticVersion.toString(appVersion));
        }

        timeUnit = snapshotLoader.timeUnit();
    }

    private long onTakeSnapshot(final long logPosition, final long leadershipTermId)
    {
        try (AeronArchive archive = AeronArchive.connect(ctx.archiveContext().clone());
            ExclusivePublication publication = aeron.addExclusivePublication(
                ctx.snapshotChannel(), ctx.snapshotStreamId()))
        {
            final String channel = ChannelUri.addSessionId(ctx.snapshotChannel(), publication.sessionId());
            archive.startRecording(channel, ctx.snapshotStreamId(), LOCAL, true);
            final CountersReader counters = aeron.countersReader();
            final int counterId = awaitRecordingCounter(publication.sessionId(), counters, archive);
            final long recordingId = RecordingPos.getRecordingId(counters, counterId);

View on GitHub (pinned to 6d60124e15)