Skip to main content
Lightweight Transactions (LWT) provide conditional updates using the Paxos consensus protocol, enabling compare-and-set operations with linearizable consistency.

What are Lightweight Transactions?

LWT allows you to make conditional modifications to data using IF clauses:
  • INSERT IF NOT EXISTS: Insert only if the row doesn’t exist
  • UPDATE IF: Update only if conditions are met
  • DELETE IF: Delete only if conditions are met
These operations are atomic and guarantee that only one client’s conditional update succeeds when multiple clients compete.
-- Insert only if not exists
INSERT INTO users (id, name) VALUES (1, 'Alice') IF NOT EXISTS;

-- Update only if current value matches
UPDATE users SET name = 'Bob' WHERE id = 1 IF name = 'Alice';

-- Delete only if condition is met  
DELETE FROM users WHERE id = 1 IF name = 'Alice';

Serial Consistency

LWT operations require setting a serial consistency level that determines how many replicas must participate in the Paxos round:
use scylla::statement::SerialConsistency;

// SERIAL - requires quorum across all datacenters
let serial = SerialConsistency::Serial;

// LOCAL_SERIAL - requires quorum in local datacenter only (recommended)
let local_serial = SerialConsistency::LocalSerial;
LocalSerial is recommended for most use cases as it provides lower latency while still ensuring linearizability within a datacenter.

Using LWT with Unprepared Statements

INSERT IF NOT EXISTS

use scylla::statement::unprepared::Statement;
use scylla::statement::SerialConsistency;

let mut statement = Statement::new(
    "INSERT INTO ks.users (id, name, email) VALUES (?, ?, ?) IF NOT EXISTS"
);
statement.set_serial_consistency(Some(SerialConsistency::LocalSerial));

let result = session
    .query_unpaged(&statement, (1_i32, "Alice", "[email protected]"))
    .await?;

// Check if the insert was applied
let rows_result = result.into_rows_result()?;
let (applied,): (bool,) = rows_result.first_row()?;

if applied {
    println!("User created successfully");
} else {
    println!("User already exists");
}

UPDATE IF

let mut statement = Statement::new(
    "UPDATE ks.accounts SET balance = balance - ? WHERE id = ? IF balance >= ?"
);
statement.set_serial_consistency(Some(SerialConsistency::LocalSerial));

let amount = 100;
let account_id = 1;

let result = session
    .query_unpaged(&statement, (amount, account_id, amount))
    .await?;

let rows_result = result.into_rows_result()?;
let (applied,): (bool,) = rows_result.first_row()?;

if applied {
    println!("Withdrawal successful");
} else {
    println!("Insufficient balance");
}

DELETE IF

let mut statement = Statement::new(
    "DELETE FROM ks.users WHERE id = ? IF name = ?"
);
statement.set_serial_consistency(Some(SerialConsistency::LocalSerial));

let result = session
    .query_unpaged(&statement, (1_i32, "Alice"))
    .await?;

let rows_result = result.into_rows_result()?;
let (applied,): (bool,) = rows_result.first_row()?;

if applied {
    println!("User deleted");
} else {
    println!("User not found or name mismatch");
}

Using LWT with Prepared Statements

Prepared statements offer better performance for LWT operations:
use scylla::statement::prepared::PreparedStatement;
use scylla::statement::SerialConsistency;

// Prepare the LWT statement
let mut prepared = session
    .prepare("INSERT INTO ks.users (id, name, email) VALUES (?, ?, ?) IF NOT EXISTS")
    .await?;

// Set serial consistency
prepared.set_serial_consistency(Some(SerialConsistency::LocalSerial));

// Execute multiple times with different values
for i in 1..=10 {
    let result = session
        .execute_unpaged(
            &prepared,
            (i, format!("user_{}", i), format!("user{}@example.com", i)),
        )
        .await?;

    let rows_result = result.into_rows_result()?;
    let (applied,): (bool,) = rows_result.first_row()?;
    
    if applied {
        println!("Created user {}", i);
    } else {
        println!("User {} already exists", i);
    }
}

ScyllaDB-Specific LWT Optimization

ScyllaDB provides optimized routing for confirmed LWT queries:
let prepared = session
    .prepare("UPDATE ks.accounts SET balance = ? WHERE id = ? IF balance >= ?")
    .await?;

// Check if this is a confirmed LWT (ScyllaDB-specific)
if prepared.is_confirmed_lwt() {
    println!("This LWT will use optimized routing");
}
ScyllaDB can detect LWT statements and apply optimizations like deterministic replica ordering for better performance. This feature is not available on Cassandra.

Processing LWT Results

LWT operations always return a result set with at least one row:

Simple Applied Check

let result = session
    .execute_unpaged(&lwt_prepared, values)
    .await?
    .into_rows_result()?;

let (applied,): (bool,) = result.first_row()?;

if applied {
    println!("Operation succeeded");
} else {
    println!("Operation failed due to condition");
}

Getting Current Values on Failure

When an LWT fails, the result includes the current column values:
let result = session
    .execute_unpaged(
        &prepared,
        (new_value, key, expected_old_value),
    )
    .await?
    .into_rows_result()?;

// Result has: [applied] + [columns used in IF condition]
let (applied, current_value): (bool, i32) = result.first_row()?;

if applied {
    println!("Update succeeded");
} else {
    println!("Update failed. Current value is: {}", current_value);
}

Full Row on IF NOT EXISTS Failure

#[derive(Debug, scylla::DeserializeRow)]
struct LwtResult {
    applied: bool,
    id: Option<i32>,
    name: Option<String>,
    email: Option<String>,
}

let result = session
    .execute_unpaged(&insert_if_not_exists_prepared, (1, "Alice", "[email protected]"))
    .await?
    .into_rows_result()?;

let lwt_result: LwtResult = result.first_row()?;

if lwt_result.applied {
    println!("User created");
} else {
    println!(
        "User already exists: {:?}, {:?}, {:?}",
        lwt_result.id, lwt_result.name, lwt_result.email
    );
}

Consistency Levels

LWT operations use two consistency levels:
  1. Serial Consistency: For the Paxos round (use set_serial_consistency())
  2. Regular Consistency: For the actual write/read after consensus (use set_consistency())
use scylla::statement::Consistency;
use scylla::statement::SerialConsistency;

let mut prepared = session
    .prepare("UPDATE ks.table SET value = ? WHERE id = ? IF value = ?")
    .await?;

// Paxos round consistency
prepared.set_serial_consistency(Some(SerialConsistency::LocalSerial));

// Final write/read consistency
prepared.set_consistency(Consistency::Quorum);

Using LWT in Batches

You can include LWT statements in batches, but all statements must be conditional:
use scylla::statement::batch::{Batch, BatchType};
use scylla::statement::SerialConsistency;

let mut batch = Batch::new(BatchType::Logged);
batch.set_serial_consistency(Some(SerialConsistency::LocalSerial));

// All statements must be LWT
batch.append_statement(
    "INSERT INTO ks.users (id, name) VALUES (?, ?) IF NOT EXISTS"
);
batch.append_statement(
    "UPDATE ks.accounts SET balance = balance - ? WHERE id = ? IF balance >= ?"
);

let batch_values = (
    (1_i32, "Alice"),
    (100, 1_i32, 100),
);

let result = session.batch(&batch, batch_values).await?;
Batch LWT operations are expensive. They require multiple Paxos rounds and should be used sparingly. Consider alternative designs if you need frequent batch LWT operations.

Complete Example

use scylla::client::session::{Session, SessionBuilder};
use scylla::statement::prepared::PreparedStatement;
use scylla::statement::SerialConsistency;
use scylla::DeserializeRow;

#[derive(Debug, DeserializeRow)]
struct Account {
    id: i32,
    balance: i32,
}

#[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?;

    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.accounts \
             (id int PRIMARY KEY, balance int)",
            &[],
        )
        .await?;

    // Prepare LWT statements
    let mut create_account = session
        .prepare("INSERT INTO examples_ks.accounts (id, balance) VALUES (?, ?) IF NOT EXISTS")
        .await?;
    create_account.set_serial_consistency(Some(SerialConsistency::LocalSerial));

    let mut withdraw = session
        .prepare(
            "UPDATE examples_ks.accounts SET balance = balance - ? \
             WHERE id = ? IF balance >= ?"
        )
        .await?;
    withdraw.set_serial_consistency(Some(SerialConsistency::LocalSerial));

    // Create account with initial balance
    let result = session
        .execute_unpaged(&create_account, (1_i32, 1000_i32))
        .await?
        .into_rows_result()?;

    let (applied,): (bool,) = result.first_row()?;
    println!("Account created: {}", applied);

    // Try to withdraw funds
    let amount = 100;
    let result = session
        .execute_unpaged(&withdraw, (amount, 1_i32, amount))
        .await?
        .into_rows_result()?;

    let (applied, current_balance): (bool, Option<i32>) = result.first_row()?;
    
    if applied {
        println!("Withdrawal of {} successful", amount);
    } else {
        println!(
            "Withdrawal failed. Current balance: {:?}",
            current_balance
        );
    }

    // Try to withdraw more than balance
    let large_amount = 2000;
    let result = session
        .execute_unpaged(&withdraw, (large_amount, 1_i32, large_amount))
        .await?
        .into_rows_result()?;

    let (applied, current_balance): (bool, Option<i32>) = result.first_row()?;
    
    if applied {
        println!("Large withdrawal successful");
    } else {
        println!(
            "Large withdrawal failed. Current balance: {:?}",
            current_balance
        );
    }

    Ok(())
}

Performance Considerations

1

Understand the Cost

LWT operations require 4 round trips (Paxos protocol) compared to 1 for regular writes. Use them only when necessary.
2

Use LocalSerial

Prefer SerialConsistency::LocalSerial over Serial for better latency in multi-datacenter deployments.
3

Minimize Contentious Updates

High contention on the same partition key can severely impact LWT performance. Design your schema to minimize conflicts.
4

Prepare Statements

Always use prepared statements for LWT operations to avoid additional parsing overhead.

Common Use Cases

  • Unique constraints: INSERT ... IF NOT EXISTS for enforcing uniqueness
  • Optimistic locking: UPDATE ... IF version = ? for version-based concurrency control
  • Account balances: UPDATE ... IF balance >= ? for safe balance deductions
  • Distributed locks: UPDATE ... IF locked = false for lock acquisition
  • State machines: UPDATE ... IF state = ? for controlled state transitions

See Also

Build docs developers (and LLMs) love