Skip to main content
Retry policies determine whether and how the driver should retry a request after it fails. The driver provides several built-in policies and supports custom implementations.

Overview

When a request fails, the retry policy decides:
  • Whether to retry the request
  • Which target to retry on (same node or next node)
  • Whether to change the consistency level
  • Whether to ignore write errors

RetryPolicy Trait

Any type implementing the RetryPolicy trait can be used:
pub trait RetryPolicy: std::fmt::Debug + Send + Sync {
    fn new_session(&self) -> Box<dyn RetrySession>;
}

pub trait RetrySession: Send + Sync {
    fn decide_should_retry(&mut self, request_info: RequestInfo) -> RetryDecision;
    fn reset(&mut self);
}

RetryDecision

The policy returns one of these decisions:
pub enum RetryDecision {
    // Retry on the same target, optionally with different consistency
    RetrySameTarget(Option<Consistency>),
    
    // Retry on the next target from load balancing plan
    RetryNextTarget(Option<Consistency>),
    
    // Don't retry, return error to user
    DontRetry,
    
    // Return empty successful response
    IgnoreWriteError,
}

DefaultRetryPolicy

The default policy retries when there’s a high chance a retry will succeed. It’s based on the DataStax Java Driver behavior.

Usage

use scylla::policies::retry::DefaultRetryPolicy;
use scylla::client::execution_profile::ExecutionProfile;
use std::sync::Arc;

let profile = ExecutionProfile::builder()
    .retry_policy(Arc::new(DefaultRetryPolicy::new()))
    .build();

Retry Behavior

The default policy handles various error types:

Connection Errors

  • BrokenConnectionError: Retries on next node (idempotent queries only)

Server Errors

  • Overloaded: Retries on next node (idempotent queries only)
  • ServerError: Retries on next node (idempotent queries only)
  • TruncateError: Retries on next node (idempotent queries only)
  • IsBootstrapping: Always retries on next node

Unavailable Errors

  • Unavailable: Retries once on next node (any query)
    • Subsequent unavailable errors are not retried

Read Timeout

  • ReadTimeout: Retries once on same node if:
    • Received >= required responses
    • data_present == false (only checksums received)

Write Timeout

  • WriteTimeout: Retries once on same node if:
    • Query is idempotent
    • write_type == BatchLog

Stream Allocation

  • UnableToAllocStreamId: Retries on next node

No Retry

These errors are never retried:
  • SyntaxError
  • Invalid
  • AlreadyExists
  • AuthenticationError
  • Unauthorized
  • ConfigError
  • ReadFailure
  • WriteFailure
  • Unprepared
  • ProtocolError

FallthroughRetryPolicy

Never retries, immediately returns errors to the user:
use scylla::policies::retry::FallthroughRetryPolicy;

let profile = ExecutionProfile::builder()
    .retry_policy(Arc::new(FallthroughRetryPolicy::new()))
    .build();
Useful for:
  • Debugging and testing
  • Applications handling retries at a higher level
  • Latency-sensitive applications preferring fast failures

DowngradingConsistencyRetryPolicy

Retries with progressively lower consistency levels:
use scylla::policies::retry::DowngradingConsistencyRetryPolicy;

let profile = ExecutionProfile::builder()
    .retry_policy(Arc::new(DowngradingConsistencyRetryPolicy::new()))
    .build();
Warning: This policy can lead to unexpected behavior and should be used with caution. It may return data from fewer replicas than requested.

Custom Retry Policy

Implement the RetryPolicy trait for custom behavior:
use scylla::policies::retry::{
    RetryPolicy, RetrySession, RetryDecision, RequestInfo
};

#[derive(Debug)]
struct MyRetryPolicy;

impl RetryPolicy for MyRetryPolicy {
    fn new_session(&self) -> Box<dyn RetrySession> {
        Box::new(MyRetrySession::new())
    }
}

struct MyRetrySession {
    retry_count: usize,
}

impl MyRetrySession {
    fn new() -> Self {
        Self { retry_count: 0 }
    }
}

impl RetrySession for MyRetrySession {
    fn decide_should_retry(&mut self, info: RequestInfo) -> RetryDecision {
        if self.retry_count >= 3 {
            return RetryDecision::DontRetry;
        }
        
        if info.is_idempotent {
            self.retry_count += 1;
            RetryDecision::RetryNextTarget(None)
        } else {
            RetryDecision::DontRetry
        }
    }

    fn reset(&mut self) {
        self.retry_count = 0;
    }
}

Request Information

The policy receives information about the failed request:
pub struct RequestInfo<'a> {
    pub error: &'a RequestAttemptError,
    pub is_idempotent: bool,
    pub consistency: Consistency,
}

Idempotency

A request is idempotent if applying it multiple times has the same effect as applying it once:
  • SELECT queries are idempotent
  • INSERT/UPDATE with unique values are NOT idempotent
  • INSERT/UPDATE with conditional clauses may be idempotent
Mark queries as idempotent when safe:
let mut query = Statement::from("SELECT * FROM table");
query.set_is_idempotent(true);

Using with Execution Profiles

Attach a retry policy via execution profile:
use scylla::client::execution_profile::ExecutionProfile;
use scylla::policies::retry::DefaultRetryPolicy;
use std::sync::Arc;

let profile = ExecutionProfile::builder()
    .retry_policy(Arc::new(DefaultRetryPolicy::new()))
    .build();

Metrics

The driver tracks retry statistics:
let metrics = session.get_metrics();
let retry_count = metrics.get_retries_num();

Best Practices

  • Use DefaultRetryPolicy for most applications
  • Mark idempotent queries appropriately
  • Avoid DowngradingConsistencyRetryPolicy unless you understand the risks
  • Consider timeouts in addition to retries
  • Use FallthroughRetryPolicy for testing and debugging
  • Monitor retry metrics to detect issues

Interaction with Other Policies

  • Load Balancing: Determines which nodes to retry on
  • Speculative Execution: Retries happen in parallel with speculative requests
  • Timeouts: Overall request timeout includes retry time

Next Steps

Build docs developers (and LLMs) love