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.
    ///
    /// # Panics

View on GitHub (pinned to 9e0d92412f)

Solutions

  1. Always derive the next epoch from the log's current epoch (current + 1 or higher) instead of a stored constant.
  2. After restoring an old data directory, bump epochs past any previously used value rather than replaying old numbers.
  3. 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

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


AI-assisted analysis of clockworklabs/SpacetimeDB@9e0d92412f (2026-08-20). Data as JSON: /api/errors/09cae406d8dd2b08. Report an issue: GitHub.