apache/cassandra · error · RuntimeException

Replacing a node without bootstrapping risks invalidating…

Error message

Replacing a node without bootstrapping risks invalidating consistency guarantees as the expected data may not be present until repair is run. To perform this operation, please restart with -Dcassandra.allow_unsafe_replace=true

What it means

checkUnsafeReplace refuses to replace a node without bootstrapping when the system property cassandra.allow_unsafe_replace is not set to true. Replacing without streaming data in can leave the ring without the expected replicas until a repair runs, so Cassandra forces the operator to opt in explicitly at startup. It is a deliberate operator-safety gate, not a bug.

Solutions

  1. If bootstrapping is intended, ensure shouldBootstrap=true is passed / the node performs a normal replace with data streaming
  2. If intentionally skipping bootstrap, restart the node with -Dcassandra.allow_unsafe_replace=true
  3. After an unsafe replace, run a full repair (nodetool repair) to restore replica consistency
  4. Never set the flag in production without a plan to repair immediately

Example fix

// before (cassandra-env.sh)
# no flag set
// after (cassandra-env.sh)
JVM_OPTS="$JVM_OPTS -Dcassandra.allow_unsafe_replace=true" // only for lab/CI clusters
Defensive patterns

Strategy: validation

Validate before calling

boolean unsafeAllowed = Boolean.getBoolean("cassandra.allow_unsafe_replace");
if (!shouldBootstrap && !unsafeAllowed)
    throw new IllegalStateException("Refusing replace-without-bootstrap: set -Dcassandra.allow_unsafe_replace=true to override");

Try / catch

try { checkUnsafeReplace(false); } catch (RuntimeException e) { promptForConfirmationAndRestartWithFlag(); }

Prevention

When it happens

Trigger: Calling BootstrapAndReplace.checkUnsafeReplace(false) (or a replace path invoking it) on a node not started with -Dcassandra.allow_unsafe_replace=true.

Common situations: Hostile take-over replace procedures, replacing a dead node in a lab where streaming is intentionally skipped, automation scripts that skip bootstrap but forgot the JVM property, operator following an old runbook for 'replace_address' without bootstrapping.

Understand the failure class

Background: "--flag is required" and "must specify" CLI errors: how missing-required-flag validation works and how to fix it — this error's family across 20 libraries.

Related errors


AI-assisted analysis of apache/cassandra@88fd0f6a0e (2026-09-10). Data as JSON: /api/errors/d1f0691251e0b221. Report an issue: GitHub.

Appendix: source

Thrown at src/java/org/apache/cassandra/tcm/sequences/BootstrapAndReplace.java:445

               Objects.equals(latestModification, that.latestModification) &&
               Objects.equals(lockKey, that.lockKey) &&
               Objects.equals(bootstrapTokens, that.bootstrapTokens) &&
               Objects.equals(startReplace, that.startReplace) &&
               Objects.equals(midReplace, that.midReplace) &&
               Objects.equals(finishReplace, that.finishReplace);
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(latestModification, lockKey, bootstrapTokens, startReplace, midReplace, finishReplace, next, finishJoiningRing, streamData);
    }

    public static void checkUnsafeReplace(boolean shouldBootstrap)
    {
        if (!shouldBootstrap && !CassandraRelevantProperties.ALLOW_UNSAFE_REPLACE.getBoolean())
        {
            throw new RuntimeException("Replacing a node without bootstrapping risks invalidating consistency " +
                                       "guarantees as the expected data may not be present until repair is run. " +
                                       "To perform this operation, please restart with " +
                                       "-Dcassandra.allow_unsafe_replace=true");
        }

    }

    public static void gossipStateToHibernate(ClusterMetadata metadata, NodeId nodeId)
    {
        if (nodeId == NodeId.UNREGISTERED)
            return;
        // order is important here, the gossiper can fire in between adding these two states.  It's ok to send TOKENS without STATUS, but *not* vice versa.
        List<Pair<ApplicationState, VersionedValue>> states = new ArrayList<>();
        VersionedValue.VersionedValueFactory valueFactory = StorageService.instance.valueFactory;
        states.add(Pair.create(ApplicationState.TOKENS, valueFactory.tokens(metadata.tokenMap.tokens(nodeId))));
        states.add(Pair.create(ApplicationState.STATUS_WITH_PORT, valueFactory.hibernate(true)));
        states.add(Pair.create(ApplicationState.STATUS, valueFactory.hibernate(true)));
        Gossiper.instance.addLocalApplicationStates(states);

View on GitHub (pinned to 88fd0f6a0e)