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
- If bootstrapping is intended, ensure shouldBootstrap=true is passed / the node performs a normal replace with data streaming
- If intentionally skipping bootstrap, restart the node with -Dcassandra.allow_unsafe_replace=true
- After an unsafe replace, run a full repair (nodetool repair) to restore replica consistency
- 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
- Always use normal (bootstrapping) replace in production
- Document that allow_unsafe_replace requires an immediate full repair
- Keep the flag out of production cassandra-env.sh
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
- Cannot replace with a node that is already bootstrapped
- A node required to move the data consistently is down
- A node with address already exists, cancelling join. Use…
- Booting with ClusterMetadata from file:
- Bootstrap can be started exactly once, but seems to have…
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)