Cassandra Batch Statements

A BATCH in Cassandra groups multiple INSERT, UPDATE, or DELETE statements into a single operation. Batches reduce network round trips by sending all statements in one request. Understanding when to use each type of batch — and when not to use batches at all — is essential for writing correct, performant Cassandra code.

BATCH Types

Type            Atomicity          Use When
──────────────────────────────────────────────────────────────
LOGGED BATCH    Yes (eventually)   Writing to multiple tables
                                   that must stay in sync
UNLOGGED BATCH  No                 Writing multiple rows to
                                   the same partition only
COUNTER BATCH   No (special)       Batching counter updates only

LOGGED BATCH

A LOGGED BATCH provides a form of atomicity: if the coordinator crashes mid-batch, the batch log (stored on replica nodes) ensures all statements eventually apply. No statement in the batch is permanently skipped. This is not the same as ACID transaction atomicity — there is no rollback, and partial states can briefly be visible during recovery.

BEGIN BATCH
  INSERT INTO orders_by_customer
    (customer_id, order_date, order_id, total, status)
    VALUES ([uuid], toTimestamp(now()), uuid(), 149.99, 'pending');

  INSERT INTO orders_by_status
    (status, order_date, order_id, customer_id, total)
    VALUES ('pending', toTimestamp(now()), [order-uuid], [uuid], 149.99);

  INSERT INTO orders_by_id
    (order_id, customer_id, total, status)
    VALUES ([order-uuid], [uuid], 149.99, 'pending');
APPLY BATCH;

How LOGGED BATCH Works Internally

Step 1: Coordinator writes the entire batch to a batch log
        table on 2 replica nodes (for safety)
Step 2: Coordinator executes each statement in the batch
Step 3: Coordinator removes the batch log entry on success
Step 4: If coordinator crashes at step 2, another node reads
        the batch log and retries all statements

The batch log adds overhead — roughly 2× the write cost. Use LOGGED BATCH only when multi-table consistency matters (like keeping denormalized tables in sync).

UNLOGGED BATCH

An UNLOGGED BATCH sends multiple statements in one network request with no atomicity guarantee and no batch log. It is significantly faster than a LOGGED BATCH but provides no recovery if the coordinator fails mid-execution.

BEGIN UNLOGGED BATCH
  INSERT INTO products (product_id, name, price)
    VALUES (uuid(), 'USB Cable',  9.99);

  INSERT INTO products (product_id, name, price)
    VALUES (uuid(), 'HDMI Cable', 14.99);

  INSERT INTO products (product_id, name, price)
    VALUES (uuid(), 'Power Bank', 39.99);
APPLY BATCH;

UNLOGGED BATCH — Same Partition Only

UNLOGGED BATCH provides the most benefit when all statements target the same partition key. In that case, all writes go to the same set of replica nodes in one message — the minimum possible network overhead.

Good: all rows in the same partition
BEGIN UNLOGGED BATCH
  INSERT INTO sensor_readings (sensor_id, day, read_time, value)
    VALUES ('S1', '2024-06-15', '2024-06-15 10:00:00', 22.5);
  INSERT INTO sensor_readings (sensor_id, day, read_time, value)
    VALUES ('S1', '2024-06-15', '2024-06-15 10:01:00', 22.6);
  INSERT INTO sensor_readings (sensor_id, day, read_time, value)
    VALUES ('S1', '2024-06-15', '2024-06-15 10:02:00', 22.4);
APPLY BATCH;

All rows have the same partition key (sensor_id='S1', day='2024-06-15')
→ coordinator sends one message to the right nodes → minimal overhead ✓

UNLOGGED BATCH — Multiple Partitions (Avoid)

When an UNLOGGED BATCH spans many different partitions across the cluster, the coordinator must send sub-batches to many different nodes. This adds more coordinator load and network traffic than sending the statements individually would. This is a common Cassandra anti-pattern known as the "multi-partition unlogged batch" problem.

Bad: rows in different partitions scattered across the cluster
BEGIN UNLOGGED BATCH
  INSERT INTO products (product_id, ...) VALUES (uuid-1, ...);  → Node A
  INSERT INTO products (product_id, ...) VALUES (uuid-2, ...);  → Node C
  INSERT INTO products (product_id, ...) VALUES (uuid-3, ...);  → Node B
  ...
  INSERT INTO products (product_id, ...) VALUES (uuid-50, ...); → Node D
APPLY BATCH;

Coordinator must fan out to 4+ nodes → MORE overhead than 50 individual writes

COUNTER BATCH

Counter columns must use a dedicated COUNTER BATCH (or a regular BATCH with only counter updates). You cannot mix counter and non-counter statements in the same batch.

BEGIN COUNTER BATCH
  UPDATE page_views SET views = views + 1 WHERE page_id = '/home';
  UPDATE page_views SET views = views + 1 WHERE page_id = '/products';
  UPDATE product_stats SET purchases = purchases + 1 WHERE product_id = [uuid];
APPLY BATCH;

Timestamps in Batches

All statements in a BATCH share the same client-side timestamp by default. This ensures that when denormalized copies of the same data are written, no copy is considered "older" due to millisecond timing differences.

BEGIN BATCH USING TIMESTAMP 1710000000000000
  INSERT INTO orders_by_customer ...;
  INSERT INTO orders_by_status   ...;
APPLY BATCH;

Batch Anti-Patterns

Anti-Pattern                         Why It Is Wrong
──────────────────────────────────────────────────────────────
Large unlogged batch across          Coordinator fan-out adds more
  many partitions                    overhead than individual writes
Batching for performance "always"    Only same-partition batches help
Using logged batch for every write   Batch log overhead on every write
Batching thousands of statements     Large batches cause coordinator
                                     memory spikes and GC pressure
Using batch as a transaction         Batch is not an ACID transaction
                                     — no rollback on failure

Batch Size Limits

Cassandra logs a warning when a batch exceeds 5 KB and fails the batch when it exceeds 50 KB (configurable in cassandra.yaml). Large batches cause coordinator memory pressure.

# cassandra.yaml
batch_size_warn_threshold_in_kb: 5    # warn above 5 KB
batch_size_fail_threshold_in_kb: 50   # fail above 50 KB

Decision Guide

Question                                   Use
──────────────────────────────────────────────────────────────
Need all writes to eventually apply?       LOGGED BATCH
All writes target same partition?          UNLOGGED BATCH
Writing to denormalized tables?            LOGGED BATCH
Updating counters only?                    COUNTER BATCH
Writing to many different partitions?      Individual writes (no batch)
Writing hundreds of different rows?        Individual async writes

Summary

Use LOGGED BATCH when writing to multiple denormalized tables that must stay in sync — its batch log provides eventual atomicity. Use UNLOGGED BATCH for multiple inserts to the same partition to save network round trips. Use COUNTER BATCH for counter-only updates. Avoid large multi-partition unlogged batches — they increase coordinator load rather than reducing it. Batches are a consistency tool, not a performance shortcut for arbitrary write groups.

Leave a Comment

Your email address will not be published. Required fields are marked *