{"record":{"id":"6ef2bd96aa87dcb6","repo":"clockworklabs/SpacetimeDB","slug":"invalid-transaction-offset-expected","errorCode":null,"errorMessage":"invalid transaction offset {}, expected {}","messagePattern":"invalid transaction offset (.+?), expected (.+?)","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/commitlog/src/segment.rs","lineNumber":127,"sourceCode":"    pub(crate) min_tx_offset: u64,\n    pub(crate) bytes_written: u64,\n\n    pub(crate) offset_index_head: Option<OffsetIndexWriter>,\n}\n\nimpl<W: io::Write> Writer<W> {\n    pub fn commit<T: Into<Transaction<U>>, U: Encode>(\n        &mut self,\n        transactions: impl IntoIterator<Item = T>,\n    ) -> io::Result<Option<Committed>> {\n        for tx in transactions {\n            let tx = tx.into();\n            let expected_offset = self.commit.min_tx_offset + self.commit.n as u64;\n            if tx.offset != expected_offset {\n                self.commit.n = 0;\n                self.commit.records.clear();\n\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidInput,\n                    format!(\"invalid transaction offset {}, expected {}\", tx.offset, expected_offset),\n                ));\n            }\n            assert!(\n                self.commit.n < u16::MAX,\n                \"maximum number of transactions in a single commit exceeded\"\n            );\n            self.commit.n += 1;\n            tx.txdata.encode_record(&mut self.commit.records);\n        }\n\n        if self.commit.n == 0 {\n            return Ok(None);\n        }\n\n        let checksum = self\n            .commit","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/commitlog/src/segment.rs#L109-L145","documentation":"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.","triggerScenarios":"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.","commonSituations":"Custom replication or replay layers that assign offsets manually instead of consuming the offsets the commitlog itself hands out.","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"],"exampleFix":"// before: hand-picked offsets with a gap\nlet txs = vec![tx(42, a), tx(44, b)]; // expected 42, 43\nwriter.commit(txs)?;\n\n// after: derive contiguous offsets from the log\nlet mut next = log.max_committed_offset().map(|o| o + 1).unwrap_or(0);\nlet txs = records.into_iter().map(|r| { let t = Transaction { offset: next, txdata: r }; next += 1; t }).collect::<Vec<_>>();\nlog.commit(txs)?;","handlingStrategy":"validation","validationCode":"fn offsets_contiguous<T>(min_tx_offset: u64, txs: &[Transaction<T>]) -> bool {\n    txs.iter().enumerate().all(|(i, t)| t.offset == min_tx_offset + i as u64)\n}\n\nlet next = log.max_committed_offset().map(|o| o + 1).unwrap_or(0);\nassert!(offsets_contiguous(next, &batch), \"batch offsets are not contiguous\");\nlog.commit(batch)?;","typeGuard":"fn is_offset_mismatch(e: &io::Error) -> bool {\n    e.kind() == io::ErrorKind::InvalidInput\n        && e.to_string().contains(\"invalid transaction offset\")\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["rust","commitlog","transaction","offset","invalid-input","sequence"],"backgroundTag":"out-of-order-sequence","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}