clockworklabs/SpacetimeDB · error · io::Error

invalid transaction offset

Error message

invalid transaction offset {}, expected {}

What it means

segment::Writer::commit enforces contiguity inside a commit batch: each transaction's offset must equal the batch's min_tx_offset plus its index within the batch. On a gap, duplicate, or out-of-order offset the pending commit buffer is reset and io::ErrorKind::InvalidInput is returned naming the bad and expected offsets.

Solutions

  1. Query the commitlog for the last committed transaction range and start assigning offsets from its end
  2. Filter and renumber transactions to a contiguous range before calling commit
  3. On retry of a partially applied batch, resume from the next uncommitted offset instead of replaying from the start
  4. Assert offsets in tests with contiguous generators (0..n) rather than hand-written literals

Example fix

// before
writer.commit([tx(3), tx(4), tx(6)])?; // gap at 5

// after
let next = log.last_tx_offset()? + 1;
let txs = [tx(3), tx(4), tx(6)]
    .into_iter()
    .enumerate()
    .map(|(i, mut tx)| { tx.offset = next + i as u64; tx });
writer.commit(txs)?;
Defensive patterns

Strategy: validation

Validate before calling

fn contiguous_from(next: u64, txs: Vec<Tx>) -> io::Result<Vec<Tx>> {
    for (i, tx) in txs.iter().enumerate() {
        let expected = next + i as u64;
        if tx.offset != expected {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("gap before batch: have {} expected {expected}", tx.offset),
            ));
        }
    }
    Ok(txs)
}

Type guard

fn is_tx_offset_mismatch(err: &io::Error) -> bool {
    err.kind() == io::ErrorKind::InvalidInput && err.to_string().starts_with("invalid transaction offset")
}

Try / catch

match writer.commit(batch) {
    Ok(committed) => Ok(committed),
    Err(e) if is_tx_offset_mismatch(&e) => {
        // writer reset its batch; rebase offsets onto the log's next offset and retry once
        let next = last_committed_offset()?;
        writer.commit(renumber_from(next, batch))
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Passing transactions whose offsets skip values (e.g. 1,2,4); retrying a batch whose first transaction already committed (duplicate offset); building the batch from a source that filtered some transactions out, leaving holes; off-by-one when assigning offsets.

Common situations: Generating offsets from user-supplied sequence numbers without normalizing; retry logic that replays a partially committed batch; consuming from an upstream system with its own gaps.

Related errors


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

Appendix: 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 3653d2ed49)