aeron-io/aeron · error · ClusterException

startupCanvassTimeoutNs=

Error message

startupCanvassTimeoutNs=<value> must be a multiple of leaderHeartbeatTimeoutNs=<value>

What it means

conclude() validates that startupCanvassTimeoutNs is at least twice leaderHeartbeatTimeoutNs (the check startupCanvassTimeoutNs / leaderHeartbeatTimeoutNs < 2). The canvass window must span multiple heartbeat intervals so election messaging can complete; a too-short canvass timeout makes leader election unreliable and is rejected.

Solutions

  1. Increase startupCanvassTimeoutNs to at least 2x (typically 5s vs 10s heartbeat: e.g. canvass 10s, heartbeat 5s)
  2. Or decrease leaderHeartbeatTimeoutNs so the ratio is >= 2
  3. Verify units — both values are nanoseconds

Example fix

// before
ctx.leaderHeartbeatTimeoutNs(5_000_000_000L)
   .startupCanvassTimeoutNs(4_000_000_000L); // < 2x
// after
ctx.leaderHeartbeatTimeoutNs(5_000_000_000L)
   .startupCanvassTimeoutNs(10_000_000_000L); // 2x heartbeat
Defensive patterns

Strategy: validation

Validate before calling

if (startupCanvassTimeoutNs / leaderHeartbeatTimeoutNs < 2) {
    throw new IllegalArgumentException("startupCanvassTimeoutNs must be >= 2x leaderHeartbeatTimeoutNs");
}

Try / catch

try { ctx.conclude(); } catch (ClusterException e) { if (e.getMessage().contains("startupCanvassTimeoutNs")) { /* fix timeout ratio in config */ } }

Prevention

When it happens

Trigger: Setting Context.startupCanvassTimeoutNs() to a value less than 2x leaderHeartbeatTimeoutNs() before conclude().

Common situations: Tuning election timeouts for fast failover and shrinking canvass below the heartbeat multiple; copying partial config from another cluster; typo putting nanosecond vs millisecond values in the wrong field.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at aeron-cluster/src/main/java/io/aeron/cluster/ConsensusModule.java:1785

                throw new UncheckedIOException(ex);
            }

            if (null == clusterMembers)
            {
                throw new ClusterException("ConsensusModule.Context.clusterMembers must be set");
            }

            if (deleteDirOnStart)
            {
                IoUtil.delete(clusterDir, false);
            }

            IoUtil.ensureDirectoryExists(clusterDir, "cluster");
            IoUtil.ensureDirectoryExists(markFileDir, "mark file");

            if (startupCanvassTimeoutNs / leaderHeartbeatTimeoutNs < 2)
            {
                throw new ClusterException(
                    "startupCanvassTimeoutNs=" + startupCanvassTimeoutNs +
                    " must be a multiple of leaderHeartbeatTimeoutNs=" + leaderHeartbeatTimeoutNs);
            }

            if (null == clusterClock)
            {
                final String clockClassName = System.getProperty(
                    CLUSTER_CLOCK_PROP_NAME, MillisecondClusterClock.class.getName());
                try
                {
                    clusterClock = (ClusterClock)Class.forName(clockClassName).getConstructor().newInstance();
                }
                catch (final Exception e)
                {
                    throw new ClusterException("failed to instantiate ClusterClock " + clockClassName, e);
                }
            }

View on GitHub (pinned to 6d60124e15)