clockworklabs/SpacetimeDB · error · io::Error

invalid transaction offset {}, expected {}

Error message

invalid transaction offset {}, expected {}

What it means

segment::Writer::commit validates every transaction in a batch: each tx.offset must equal the segment's min_tx_offset plus its index within the batch. A gap, duplicate, or regression resets the pending batch (records cleared, count zeroed) and returns InvalidInput showing the offending and expected offsets. This enforces the log's contiguous-offset invariant before anything is written.

Source

Thrown at crates/commitlog/src/segment.rs:127

    pub(crate) min_tx_offset: u64,
    pub(crate) bytes_written: u64,

    pub(crate) offset_index_head: Option<OffsetIndexWriter>,
}

impl<W: io::Write> Writer<W> {
    pub fn commit<T: Into<Transaction<U>>, U: Encode>(
        &mut self,
        transactions: impl IntoIterator<Item = T>,
    ) -> io::Result<Option<Committed>> {
        for tx in transactions {
            let tx = tx.into();
            let expected_offset = self.commit.min_tx_offset + self.commit.n as u64;
            if tx.offset != expected_offset {
                self.commit.n = 0;
                self.commit.records.clear();

                return Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    format!("invalid transaction offset {}, expected {}", tx.offset, expected_offset),
                ));
            }
            assert!(
                self.commit.n < u16::MAX,
                "maximum number of transactions in a single commit exceeded"
            );
            self.commit.n += 1;
            tx.txdata.encode_record(&mut self.commit.records);
        }

        if self.commit.n == 0 {
            return Ok(None);
        }

        let checksum = self
            .commit

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Derive the start offset from the log (max_committed_offset() + 1, or the segment's min_tx_offset for a fresh segment) and number transactions sequentially
  2. Validate the batch is contiguous before calling commit
  3. Prefer the higher-level Commitlog::commit API, which manages offset assignment for you

Example fix

// before: hand-picked offsets with a gap
let txs = vec![tx(42, a), tx(44, b)]; // expected 42, 43
writer.commit(txs)?;

// after: derive contiguous offsets from the log
let mut next = log.max_committed_offset().map(|o| o + 1).unwrap_or(0);
let txs = records.into_iter().map(|r| { let t = Transaction { offset: next, txdata: r }; next += 1; t }).collect::<Vec<_>>();
log.commit(txs)?;
Defensive patterns

Strategy: validation

Validate before calling

fn offsets_contiguous<T>(min_tx_offset: u64, txs: &[Transaction<T>]) -> bool {
    txs.iter().enumerate().all(|(i, t)| t.offset == min_tx_offset + i as u64)
}

let next = log.max_committed_offset().map(|o| o + 1).unwrap_or(0);
assert!(offsets_contiguous(next, &batch), "batch offsets are not contiguous");
log.commit(batch)?;

Type guard

fn is_offset_mismatch(e: &io::Error) -> bool {
    e.kind() == io::ErrorKind::InvalidInput
        && e.to_string().contains("invalid transaction offset")
}

Prevention

When it happens

Trigger: Hand-building Transaction batches whose offsets don't start at or continue the segment's next offset; reusing stale offsets after a reset_to; mixing transactions sourced from another segment; off-by-one when computing the batch's starting offset.

Common situations: Custom replication or replay layers that assign offsets manually instead of consuming the offsets the commitlog itself hands out.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/6ef2bd96aa87dcb6. Report an issue: GitHub.