Skip to main content
Batch statements allow you to execute multiple CQL statements (prepared or unprepared) in a single request, providing atomicity guarantees depending on the batch type.

Batch Types

ScyllaDB supports three types of batches:
use scylla::statement::batch::BatchType;

// LOGGED batch - atomic and isolated (default)
let logged_batch = Batch::new(BatchType::Logged);

// UNLOGGED batch - not atomic, better performance
let unlogged_batch = Batch::new(BatchType::Unlogged);

// COUNTER batch - for counter updates only
let counter_batch = Batch::new(BatchType::Counter);

Logged Batches

  • Atomic: All statements succeed or all fail
  • Isolated: Batch appears to execute instantaneously from other operations’ perspective
  • Performance: Slower due to batch log overhead
  • Use for: Ensuring data consistency across multiple writes

Unlogged Batches

  • Not Atomic: Some statements may succeed while others fail
  • Performance: Faster, no batch log
  • Use for: Grouping independent writes to the same partition for efficiency

Counter Batches

  • Specialized: Only for counter updates
  • Use for: Batching multiple counter increments/decrements

Creating Batches

Empty Batch

use scylla::statement::batch::{Batch, BatchType};

// Create an empty batch
let mut batch = Batch::new(BatchType::Logged);

// Or use default (Logged)
let mut batch: Batch = Default::default();

Batch with Statements

use scylla::statement::batch::{Batch, BatchType, BatchStatement};

let statements = vec![
    BatchStatement::from("INSERT INTO ks.tab(a, b) VALUES(?, ?)"),
    BatchStatement::from("INSERT INTO ks.tab(a, b) VALUES(3, ?)"),
];

let batch = Batch::new_with_statements(BatchType::Logged, statements);

Adding Statements

You can add both unprepared and prepared statements to a batch:
use scylla::statement::batch::Batch;
use scylla::statement::unprepared::Statement;

let mut batch: Batch = Default::default();

// Add unprepared statement from &str
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(?, ?)");

// Add unprepared Statement
let statement = Statement::new("INSERT INTO ks.tab(a, b) VALUES(3, ?)");
batch.append_statement(statement);

// Add prepared statement
let prepared = session
    .prepare("INSERT INTO ks.tab(a, b) VALUES(5, ?)")
    .await?;
batch.append_statement(prepared);

// Add statement with no values
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(7, 8)");
For maximum performance, prefer using prepared statements in batches. Unprepared statements with non-empty values will be prepared automatically, requiring additional round trips.

Executing Batches

Batch values must be provided for each statement in the batch:
let mut batch: Batch = Default::default();

// Statement with two bound values
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(?, ?)");

// Statement with one bound value  
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(3, ?)");

// Statement with no bound values
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(5, 6)");

// Batch values: tuple of tuples, one per statement
let batch_values = (
    (1_i32, 2_i32),  // Two values for first statement
    (4_i32,),        // One value for second statement  
    (),              // No values for third statement
);

// Execute the batch
session.batch(&batch, batch_values).await?;

Batch Values

The driver accepts any type implementing the BatchValues trait. Common patterns:

Tuple of Tuples

// Each inner tuple corresponds to one statement
let batch_values = (
    (1_i32, "text1"),     // Statement 1
    (2_i32, "text2"),     // Statement 2  
    (3_i32, "text3"),     // Statement 3
);

session.batch(&batch, batch_values).await?;

Vector of Tuples

// Useful for dynamic number of statements
let batch_values: Vec<(i32, &str)> = vec![
    (1, "text1"),
    (2, "text2"),
    (3, "text3"),
];

session.batch(&batch, batch_values).await?;

Mixed Statement Types

let mut batch: Batch = Default::default();

// Prepared statement with 2 values
let prepared = session
    .prepare("INSERT INTO ks.tab(a, b) VALUES(?, ?)")
    .await?;
batch.append_statement(prepared);

// Unprepared statement with 1 value
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(3, ?)");

// Unprepared statement with no values
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(5, 6)");

let batch_values = (
    (1_i32, 2_i32),  // For prepared statement
    (4_i32,),        // For first unprepared statement
    (),              // For second unprepared statement
);

session.batch(&batch, batch_values).await?;

Configuration

Batches support similar configuration options as individual statements:

Consistency Levels

use scylla::statement::Consistency;
use scylla::statement::SerialConsistency;

let mut batch: Batch = Default::default();

// Set consistency
batch.set_consistency(Consistency::Quorum);

// Set serial consistency for LWT batches
batch.set_serial_consistency(Some(SerialConsistency::LocalSerial));

// Get consistency values
let consistency = batch.get_consistency();
let serial_consistency = batch.get_serial_consistency();

Request Timeout

use std::time::Duration;

let mut batch: Batch = Default::default();
batch.set_request_timeout(Some(Duration::from_secs(30)));

Timestamp

Set a default timestamp for all statements in the batch:
let mut batch: Batch = Default::default();

// Set timestamp in microseconds
let timestamp_micros = 1234567890;
batch.set_timestamp(Some(timestamp_micros));

Idempotence

let mut batch: Batch = Default::default();

// Mark batch as idempotent for safe retries
batch.set_is_idempotent(true);

Tracing

let mut batch: Batch = Default::default();
batch.set_tracing(true);

let result = session.batch(&batch, batch_values).await?;
if let Some(tracing_id) = result.tracing_id() {
    println!("Tracing ID: {}", tracing_id);
}

Token-Aware Routing

Batches use the first prepared statement to determine routing:
let mut batch: Batch = Default::default();

// First statement determines routing
let prepared = session
    .prepare("INSERT INTO ks.tab(partition_key, value) VALUES(?, ?)")
    .await?;
batch.append_statement(prepared);

// Add more statements...
batch.append_statement("INSERT INTO ks.tab(partition_key, value) VALUES(?, ?)");

let batch_values = (
    (1_i32, "value1"),  // This partition key determines routing
    (2_i32, "value2"),
);

session.batch(&batch, batch_values).await?;
If you batch statements by partition key (all statements affect the same partition), you’ll get optimal shard-aware routing. Mixing different partition keys in a batch may result in cross-shard communication.

Best Practices

1

Use Prepared Statements

Always prefer prepared statements in batches for better performance. Unprepared statements with values require additional preparation round trips.
2

Batch Same-Partition Writes

For best performance with unlogged batches, group writes to the same partition together to benefit from shard-aware routing.
3

Limit Batch Size

Keep batch sizes reasonable (typically under 100 statements). Very large batches can cause performance issues and timeouts.
4

Choose Appropriate Type

Use logged batches only when you need atomicity guarantees. For independent writes, unlogged batches are faster.

Complete Example

use scylla::client::session::{Session, SessionBuilder};
use scylla::statement::batch::{Batch, BatchType};
use scylla::statement::Consistency;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let session: Session = SessionBuilder::new()
        .known_node("127.0.0.1:9042")
        .build()
        .await?;

    // Create schema
    session
        .query_unpaged(
            "CREATE KEYSPACE IF NOT EXISTS examples_ks \
             WITH REPLICATION = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}",
            &[],
        )
        .await?;

    session
        .query_unpaged(
            "CREATE TABLE IF NOT EXISTS examples_ks.tab \
             (a int, b int, primary key (a))",
            &[],
        )
        .await?;

    // Prepare statement for batch
    let prepared = session
        .prepare("INSERT INTO examples_ks.tab(a, b) VALUES(?, ?)")
        .await?;

    // Create and configure batch
    let mut batch = Batch::new(BatchType::Logged);
    batch.set_consistency(Consistency::Quorum);

    // Add statements
    batch.append_statement(prepared.clone());
    batch.append_statement(prepared.clone());
    batch.append_statement(prepared);

    // Execute batch with values
    let batch_values = (
        (1_i32, 10_i32),
        (2_i32, 20_i32),
        (3_i32, 30_i32),
    );

    session.batch(&batch, batch_values).await?;
    println!("Batch executed successfully");

    Ok(())
}

Common Pitfalls

Avoid large batches: Batches with more than a few hundred statements can cause coordinator node overload and timeouts.
Avoid batching single-partition writes: If all writes target the same partition, a single INSERT or UPDATE with a collection is more efficient.
Don’t use batches for reads: Batches are for writes only. Use multiple async queries for parallel reads.

See Also

  • Prepared Statements - Optimize batch performance with prepared statements
  • Values - How to bind values in batches
  • LWT - Using batches with lightweight transactions
  • Timeouts - Configuring batch timeouts

Build docs developers (and LLMs) love