Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Cassandra Failure Handling Policies

Cassandra drivers implement sophisticated failure handling to maintain availability despite node failures, network issues, and transient errors. This includes retry policies, speculative execution, and idempotency awareness.


Traditional database drivers (JDBC, ODBC, database-specific libraries) provide minimal failure handling intelligence. Error recovery is almost entirely the application's responsibility:

Traditional RDBMS: Application-Level Error HandlingTraditional RDBMS: Application-Level Error HandlingApplicationRDBMS DriverDatabaseBusiness LogicError Handling(custom code)Retry Logic(custom code)Connection Recovery(custom code)Failover Logic(custom code)Connection PoolQuery ExecutionError TranslationPrimaryReplica(read-only)Application must implement:- Retry with backoff- Connection validation- Failover logic- Timeout handling- Error classificationDriver only provides:- Connection management- Query execution- Error code translationSQLExceptionthrow exceptionif recoverableretryif connection lostreconnectif primary downmanual failover

What RDBMS Drivers Typically Provide:

FeatureRDBMS DriverApplication Must Handle
Connection poolingBasic poolValidation, sizing, recovery
Error reportingRaw exceptionsClassification, retry decisions
FailoverNoneManual primary/replica switching
Retry logicNoneExponential backoff, limits
Timeout handlingBasicAppropriate values, recovery
Load balancingNone (or round-robin)Intelligent routing
Health monitoringNoneHeartbeats, connection testing

Common Application-Level Retry Pattern (RDBMS):

# Application must implement retry logic
def execute_with_retry(connection_pool, query, max_retries=3):
last_exception = None
for attempt in range(max_retries):
try:
conn = connection_pool.get_connection()
# Must validate connection isn't stale
if not validate_connection(conn):
conn = create_new_connection()
result = conn.execute(query)
return result
except ConnectionError as e:
last_exception = e
# Application decides what to do
connection_pool.invalidate(conn)
time.sleep(2 ** attempt) # Manual backoff
except DatabaseError as e:
if is_transient_error(e): # Application classifies
last_exception = e
time.sleep(2 ** attempt)
else:
raise # Non-recoverable
raise last_exception

Cassandra drivers embed sophisticated failure handling that would require thousands of lines of custom application code with traditional databases:

Cassandra Driver: Built-in Failure IntelligenceCassandra Driver: Built-in Failure IntelligenceApplicationCassandra DriverCassandra ClusterBusiness LogicOnly handlesbusiness errorsRetry PolicySpeculative ExecutionLoad BalancingConnection Pool(per node)Health MonitoringReconnection(exponential backoff)Circuit BreakerIdempotency TrackingToken-Aware RoutingNode 1Node 2Node 3Driver automatically handles:- Error classification- Retry decisions- Node selection- Failover- Backpressure- Health trackingtimeout/errorslow responsespeculativefailurestrip

Built-in Cassandra Driver Capabilities:

FeatureCassandra Driver Provides
Retry PoliciesConfigurable policies with error-type awareness
Speculative ExecutionParallel requests for tail latency reduction
Load BalancingToken-aware, DC-aware, latency-aware routing
Connection ManagementPer-node pools with automatic scaling
Health MonitoringContinuous heartbeats, state tracking
ReconnectionExponential backoff with configurable limits
Circuit BreakersNode-level failure isolation (driver-dependent)
Idempotency AwarenessSafe retry decisions based on operation type (driver-dependent)
Topology AwarenessAutomatic discovery, rack/DC awareness
Metadata SyncSchema and token ring synchronization
RDBMS and Cassandra client stacks comparedRDBMS and Cassandra client stacks comparedRDBMS StackCassandra StackApplication Code(custom error handling)RDBMS Driver(minimal)Load Balancer(HAProxy/F5)PrimaryReplicaApplication Code(driver configuration)Cassandra Driver(intelligent)Node 1Node 2Node 3
AspectRDBMSCassandra
Error handling codeSignificant custom codePrimarily configuration
Failover implementationManual/customAutomatic
Retry logicApplication responsibilityDriver policy
Node health trackingExternal monitoringBuilt-in
Load balancingExternal (HAProxy, etc.)Built-in
Connection recoveryManual validationAutomatic reconnection
Timeout handlingPer-query codePolicy-based
Speculative executionNot availableBuilt-in option

Driver Configuration Over Custom Code

With Cassandra drivers, failure handling is configured rather than coded. Instead of implementing retry loops, connection validation, and failover logic, applications configure policies that the driver executes automatically. This reduces application complexity and ensures consistent, tested behavior.


Failure TypeScopeRecovery Strategy
Connection failureSingle connectionReconnect, try other node
Request timeoutSingle requestRetry based on policy
Node downSingle nodeRoute to other nodes
Coordinator errorRequest processingRetry on same/different node
Consistency failureCluster-wideMay not be recoverable
Driver retry flow across nodes after read timeoutsApplicationDriverRetry PolicyNode ANode BApplicationApplicationDriverDriverRetry PolicyRetry PolicyNode ANode ANode BNode Bexecute(query)requestTimeoutonReadTimeout(...)RETRY_SAME_HOSTretryTimeoutonReadTimeout(...)RETRY_NEXT_HOSTretrySuccessResult

# Conceptual retry policy interface
class RetryPolicy:
def on_read_timeout(self, statement, consistency, required, received,
data_retrieved, retry_number):
"""Called when a read times out"""
return RetryDecision.RETHROW # or RETRY_SAME, RETRY_NEXT, IGNORE
def on_write_timeout(self, statement, consistency, write_type,
required, received, retry_number):
"""Called when a write times out"""
return RetryDecision.RETHROW
def on_unavailable(self, statement, consistency,
required, alive, retry_number):
"""Called when not enough replicas available"""
return RetryDecision.RETHROW
def on_request_error(self, statement, consistency, exception, retry_number):
"""Called on other request errors"""
return RetryDecision.RETHROW
DecisionBehavior
RETHROWPropagate error to application
RETRY_SAME_HOSTRetry on same coordinator
RETRY_NEXT_HOSTRetry on next node in query plan
IGNOREReturn empty result (for reads)

Conservative policy that retries only when safe:

ErrorRetry?Rationale
Read timeout (data received)Yes, same hostCoordinator has data
Read timeout (no data)NoMight not be available
Write timeout (BATCH_LOG)Yes, same hostSafe to retry
Write timeout (other)NoRisk of duplicate writes
UnavailableNoWon't succeed

Never retry (application handles everything):

class FallthroughRetryPolicy:
def on_read_timeout(self, *args):
return RetryDecision.RETHROW
def on_write_timeout(self, *args):
return RetryDecision.RETHROW
def on_unavailable(self, *args):
return RetryDecision.RETHROW

Retries at lower consistency when needed:

class DowngradingConsistencyPolicy:
def on_unavailable(self, statement, consistency, required, alive, retry_num):
if retry_num > 0:
return RetryDecision.RETHROW
# Downgrade to match available replicas
if alive >= 1:
if consistency in [QUORUM, LOCAL_QUORUM]:
return RetryDecision.RETRY_SAME_HOST_WITH_CONSISTENCY(ONE)
return RetryDecision.RETHROW

Consistency Violation Risk

Downgrading consistency can result in stale reads or lost writes. This policy should only be used when availability is prioritized over consistency, and the application can tolerate eventual consistency.


Coordinator didn't receive enough responses in time:

ReadTimeoutException:
consistency: QUORUM
required: 2
received: 1
data_retrieved: false
Interpretation:
- 2 responses needed for QUORUM
- Only 1 replica responded
- No data was retrieved

Retry strategy:

  • If data_retrieved=true: Safe to retry same host
  • If data_retrieved=false: Retry may not help

Coordinator didn't receive enough acknowledgments:

WriteTimeoutException:
consistency: QUORUM
required: 2
received: 1
write_type: SIMPLE
Write types:
SIMPLE - Single partition write
BATCH - Atomic batch
BATCH_LOG - Batch log write
UNLOGGED_BATCH - Non-atomic batch
COUNTER - Counter update
CAS - Compare-and-set (LWT)

Retry strategy by write_type:

Write TypeSafe to Retry?Reason
BATCH_LOGYesBatch logged, will complete
SIMPLEMaybe*Depends on idempotency
BATCHMaybe*Depends on idempotency
UNLOGGED_BATCHMaybe*Some writes may have succeeded
COUNTERNoNon-idempotent
CASNoMay have succeeded

*Idempotent operations only

Not enough replicas alive to satisfy consistency:

UnavailableException:
consistency: QUORUM
required: 2
alive: 1
Interpretation:
- Cluster knows only 1 replica is up
- Can't attempt the operation
- Request never sent to replicas

Retry strategy:

  • Retry won't help unless topology changes
  • Consider downgrading consistency
  • May indicate larger cluster issue

Connection or protocol level failures:

ErrorRetry Appropriate?
Connection closedYes, different node
Protocol errorNo (bug)
Server errorMaybe, different node
OverloadedYes, with backoff

Send redundant requests to reduce tail latency:

Speculative execution against a second node after a delay thresholdDriverDriverNode ANode ANode BNode BDriverDriverNode ANode ANode BNode BrequestStart timer50ms delay thresholdspeculative requestFirst hasn't respondedresponse (ignored if B first)response (used if first)

Speculative execution helps when:

  • Occasional slow nodes
  • Network hiccups
  • GC pauses on nodes
  • Uneven load distribution

Non-Idempotent Operations

Speculative execution should only be used for idempotent operations. Non-idempotent writes may be executed multiple times, causing data inconsistency.

# Conceptual speculative execution policy
class SpeculativeExecutionPolicy:
def new_plan(self, keyspace, statement):
"""Return when to start speculative requests"""
return SpeculativePlan(
delay_ms=50, # Wait 50ms before speculating
max_speculative=2 # At most 2 speculative requests
)

Constant Delay:

Start speculative after fixed delay
Example: 50ms, 100ms thresholds

Percentile-Based:

Start speculative at p99 latency
Adapts to observed performance
Requires latency tracking

No Speculation:

Never send speculative requests
Simplest, safest option

Speculative execution and retries are different:

AspectRetrySpeculative
TriggerError receivedTimeout threshold
Original requestAbandonedStill pending
GoalError recoveryLatency reduction
Request countSerialParallel

Non-idempotent operations may cause problems when retried:

Non-idempotent:
counter += 1 → Retry doubles increment
Conditionally idempotent:
INSERT IF NOT EXISTS → Data outcome is idempotent (won't change after success)
but applied flag may differ on retry
Idempotent:
SET value = 5 → Retry is safe
DELETE WHERE... → Retry is safe

LWT and Retries

Lightweight transactions (IF clauses) are generally safe to retry for data correctness—retrying after an unknown outcome won't corrupt data. However, the wasApplied() result may differ between the original and retry, requiring careful handling in application logic.

Design for Idempotency

Design write operations to be idempotent whenever possible. Use absolute values (SET x = 5) rather than increments (SET x = x + 1) to enable safe retries.

Drivers can track idempotency:

# Mark statement as idempotent
statement = SimpleStatement(
"UPDATE users SET name = ? WHERE id = ?",
is_idempotent=True
)
# Prepared statements can have default
prepared = session.prepare("UPDATE users SET name = ? WHERE id = ?")
prepared.is_idempotent = True
class IdempotentAwareRetryPolicy:
def on_write_timeout(self, statement, consistency, write_type,
required, received, retry_number):
if statement.is_idempotent:
return RetryDecision.RETRY_NEXT_HOST
else:
return RetryDecision.RETHROW
OperationIdempotent?Make Idempotent
INSERTYes*Use fixed values
UPDATE SET x = 5YesN/A
UPDATE SET x = x + 1NoUse LWT or external tracking
DELETEYesN/A
Counter updateNoCan't easily
LWT (IF...)NoApplication must handle

*INSERT with same PK is idempotent (upsert behavior)


Prevent overwhelming failing nodes:

Node-level circuit breaker statesNode-level circuit breaker statesClosedNormal routingOpenSkip this nodeHalf-OpenTest node5 failures in 10s30s timeoutTest failsTest succeeds
class NodeCircuitBreaker:
def __init__(self, failure_threshold=5, open_duration=30):
self.failures = 0
self.state = "CLOSED"
self.last_failure = None
self.failure_threshold = failure_threshold
self.open_duration = open_duration
def record_success(self):
self.failures = 0
self.state = "CLOSED"
def record_failure(self):
self.failures += 1
self.last_failure = time.now()
if self.failures >= self.failure_threshold:
self.state = "OPEN"
def should_try(self):
if self.state == "CLOSED":
return True
if self.state == "OPEN":
if time.now() - self.last_failure > self.open_duration:
self.state = "HALF_OPEN"
return True
return False
return True # HALF_OPEN - allow test

Connection reset by peer:
1. Mark connection dead
2. Remove from pool
3. Retry on different connection
4. Schedule reconnection
Coordinator crashes mid-request:
1. Connection closes
2. Request fails with error
3. Retry on different node
4. Idempotent operations safe

Some replicas succeed, others fail:

Write to 3 replicas, 2 succeed:
- Client may see WriteTimeoutException
- But 2 copies exist
- Read at QUORUM will succeed
- Retry may create 4th copy (okay for idempotent)

LWT requires special handling:

CAS operation timeout:
- May have succeeded
- May have failed
- Retry may see "already exists"
- Application must handle all cases

Connection timeout: Time to establish TCP connection
Request timeout: Total time for request completion
Read timeout: Time waiting for coordinator response
# Typical timeout configuration
cluster = Cluster(
connect_timeout=5, # Connection establishment
request_timeout=12 # Overall request timeout
)
# Per-statement timeout
statement = SimpleStatement(
"SELECT * FROM large_table",
timeout=60 # Override for slow query
)
WorkloadRequest TimeoutRationale
OLTP1-5sFast failure, retry elsewhere
Analytics60-300sLong-running queries
Batch load30-60sLarge writes

MetricHealthy RangeAlert Threshold
Retry rate< 1%> 5%
Speculative execution rate< 5%> 20%
Circuit breaker opens0Any
Timeout rate< 0.1%> 1%
# Track failure handling stats
class FailureMetrics:
def __init__(self):
self.retries = Counter()
self.speculative = Counter()
self.circuit_opens = Counter()
self.errors_by_type = Counter()
def on_retry(self, error_type, node):
self.retries.inc(error_type=error_type, node=node)
def on_speculative(self, node):
self.speculative.inc(node=node)

Use CaseRecommended Policy
General productionDefault policy
Strict consistencyFallthrough (handle in app)
High availabilityDowngrading (with caution)
Idempotent workloadAggressive retry
from cassandra import WriteTimeoutException, UnavailableException
try:
session.execute(statement)
except WriteTimeoutException as e:
if statement.is_idempotent:
logger.warning(f"Write timeout, may have succeeded: {e}")
# Retry or verify
else:
logger.error(f"Write timeout, state unknown: {e}")
# Manual intervention may be needed
except UnavailableException as e:
logger.error(f"Cluster unhealthy: {e}")
# Alert operations team
  1. Mark idempotency explicitly - Don't rely on inference
  2. Set appropriate timeouts - Not too short, not too long
  3. Monitor failure rates - Catch issues early
  4. Test failure scenarios - Chaos engineering
  5. Document retry behavior - Operations team awareness