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
.commitView on GitHub (pinned to 524b4487d9)
Solutions
- 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
- Validate the batch is contiguous before calling commit
- 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
- Never hand-assign transaction offsets; derive them from max_committed_offset() + 1 or the segment's next_tx_offset()
- Keep offset allocation in one component so gaps cannot be introduced by multiple code paths
- Recompute the base offset after every reset_to or reset
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
- InvalidInput
- InvalidInput
- InvalidInput
- error reading commit header: {e}
- failed to read {} bytes of commit payload: {}
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/6ef2bd96aa87dcb6.
Report an issue: GitHub.