clockworklabs/SpacetimeDB · error · std::io::Error
new epoch is smaller than current epoch
Error message
new epoch is smaller than current epoch
What it means
Commitlog::set_epoch enforces that epochs never regress: the epoch is a monotonically increasing term number (leader fencing in distributed deployments), and passing a value below the current head epoch returns io::ErrorKind::InvalidInput with this message without changing anything. Setting the same epoch again is an accepted no-op.
Source
Thrown at crates/commitlog/src/commitlog.rs:112
/// Get the current epoch.
///
/// See also: [`Commit::epoch`].
pub fn epoch(&self) -> u64 {
self.head.commit.epoch
}
/// Update the current epoch.
///
/// Does nothing if the given `epoch` is equal to the current epoch.
///
/// # Errors
///
/// If `epoch` is smaller than the current epoch, an error of kind
/// [`io::ErrorKind::InvalidInput`] is returned.
pub fn set_epoch(&mut self, epoch: u64) -> io::Result<()> {
if epoch < self.head.epoch() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"new epoch is smaller than current epoch",
));
}
self.head.set_epoch(epoch);
Ok(())
}
/// Force the currently active segment to be flushed to storage.
///
/// Using a filesystem backend, this means to call `fsync(2)`.
///
/// **Note** that this does not flush the buffered data from calls to
/// [Self::commit], it only instructs the underlying storage to flush its
/// buffers. Call [Self::flush] prior to this method to ensure data from
/// all previous [Self::commit] calls is flushed to the underlying storage.
///
/// # PanicsView on GitHub (pinned to 9e0d92412f)
Solutions
- Always derive the next epoch from the log's current epoch (current + 1 or higher) instead of a stored constant.
- After restoring an old data directory, bump epochs past any previously used value rather than replaying old numbers.
- On this error, read the current head epoch from the log and retry with a strictly larger value.
Example fix
// before commitlog.set_epoch(previous_epoch)?; // after let next = current_epoch.max(previous_epoch) + 1; commitlog.set_epoch(next)?;
Defensive patterns
Strategy: validation
Validate before calling
// Guard before calling set_epoch: epochs must never decrease
if new_epoch < current_epoch {
anyhow::bail!("refusing to regress epoch {current_epoch} -> {new_epoch}");
}
commitlog.set_epoch(new_epoch)?; Try / catch
match commitlog.set_epoch(epoch) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::InvalidInput => {
// recompute from the log's current epoch and retry once with a strictly larger value
let bumped = current_epoch + 1;
commitlog.set_epoch(bumped)?;
}
Err(e) => return Err(e.into()),
} Prevention
- Keep one source of truth for the current epoch (the log head) and always derive the next as head + 1.
- Never initialize epochs from stale config after a restore - query the log first.
- In tests, use a fresh commitlog directory when resetting epochs to zero.
When it happens
Trigger: Calling commitlog.set_epoch(n) with n smaller than the epoch already recorded in the head segment - e.g. replaying stale coordinator state, restoring an older data directory alongside newer epoch bookkeeping, or computing the next epoch from a cached value.
Common situations: Leader/failover logic that recomputes epochs from stale config; restore-from-backup followed by re-running the promotion sequence; test harnesses resetting epoch to 0 against an existing log.
Related errors
- failed to read {} bytes of commit payload: {}
- failed to read checksum: {e}
- Error looking up name for truncated table {table_id:?}
- Delete for non-existent row when replaying transaction
- Invalid system variable {s}
AI-assisted analysis of clockworklabs/SpacetimeDB@9e0d92412f (2026-08-20).
Data as JSON: /api/errors/09cae406d8dd2b08.
Report an issue: GitHub.