apache/cassandra · error · InvalidRequestException

Cannot include a counter statement in a logged batch

Error message

Cannot include a counter statement in a logged batch

What it means

Counter mutations are not safe in logged batches: logged batches write to the batchlog for atomicity, but counter updates are not idempotent and cannot be replayed safely. validate() therefore rejects any logged batch (default BATCH without UNLOGGED flag) that contains a counter statement.

Source

Thrown at src/java/org/apache/cassandra/cql3/statements/BatchStatement.java:300

                hasNonCounters = true;

            if (statement.isVirtual())
                hasVirtualTables = true;
            else
                hasRegularTables = true;
        }

        if (timestampSet && hasCounters)
            throw new InvalidRequestException("Cannot provide custom timestamp for a BATCH containing counters");

        if (isCounter() && hasNonCounters)
            throw new InvalidRequestException("Cannot include non-counter statement in a counter batch");

        if (hasCounters && hasNonCounters)
            throw new InvalidRequestException("Counter and non-counter mutations cannot exist in the same batch");

        if (isLogged() && hasCounters)
            throw new InvalidRequestException("Cannot include a counter statement in a logged batch");

        if (isLogged() && hasVirtualTables)
            throw new InvalidRequestException("Cannot include a virtual table statement in a logged batch");

        if (hasVirtualTables && hasRegularTables)
            throw new InvalidRequestException("Mutations for virtual and regular tables cannot exist in the same batch");

        if (hasConditions && hasVirtualTables)
            throw new InvalidRequestException("Conditional BATCH statements cannot include mutations for virtual tables");

        if (hasConditions)
        {
            String ksName = null;
            String cfName = null;
            for (ModificationStatement stmt : statements)
            {
                if (ksName != null && (!stmt.keyspace().equals(ksName) || !stmt.table().equals(cfName)))
                    throw new InvalidRequestException("Batch with conditions cannot span multiple tables: " + stmt.source);

View on GitHub (pinned to 88fd0f6a0e)

Solutions

  1. Use BEGIN UNLOGGED BATCH for batches containing counter statements
  2. Prefer executing counter mutations outside of batches, or in a dedicated UNLOGGED batch containing only counters
  3. Reconsider using counters if atomic multi-write semantics are required — counters cannot be batched atomically

Example fix

// before
BEGIN BATCH UPDATE stats SET hits = hits + 1 WHERE id = 1; APPLY BATCH;
// after
BEGIN UNLOGGED BATCH UPDATE stats SET hits = hits + 1 WHERE id = 1; APPLY BATCH;
Defensive patterns

Strategy: validation

Validate before calling

if (statements.stream().anyMatch(s -> isCounterTable(s.getTable())) && batchType == BatchType.LOGGED)
    batchType = BatchType.UNLOGGED; // counters require unlogged batches

Try / catch

catch (InvalidRequestException e) { if (e.getMessage().contains("logged batch")) { retryAsUnlogged(); } else throw e; }

Prevention

When it happens

Trigger: 'BEGIN BATCH UPDATE counter_table SET c = c + 1 ...; APPLY BATCH' — i.e. a counter mutation inside a batch that is not declared UNLOGGED.

Common situations: Developers forgetting the counter/logged-batch rule; frameworks generating plain BEGIN BATCH for all writes; schema changed to counters after batch code was written.

Related errors


AI-assisted analysis of apache/cassandra@88fd0f6a0e (2026-09-10). Data as JSON: /api/errors/44d7aff5efaa6e43. Report an issue: GitHub.