temporalio/temporal · error

%w: state transition count < ref transition count: %v < %v

Error message

%w: state transition count < ref transition count: %v < %v

What it means

When the reference's failover version falls within the last history entry's version range, StalenessCheck additionally compares transition counts. If the state's maximum transition count in that range is below the reference's transition count, the state is behind the reference within the same failover epoch, and consts.ErrStaleState is returned wrapped with both counts.

Source

Thrown at common/persistence/transitionhistory/transition_history.go:107

	if idx == -1 {
		lastItem := history[len(history)-1]
		if lastItem.NamespaceFailoverVersion < refVersionedTransition.NamespaceFailoverVersion {
			return fmt.Errorf(
				"%w: state namespace failover version < ref namespace failover version: %v < %v",
				consts.ErrStaleState,
				lastItem.NamespaceFailoverVersion,
				refVersionedTransition.NamespaceFailoverVersion,
			)
		}
		return fmt.Errorf(
			"%w: state namespace failover version > ref namespace failover version: %v > %v",
			consts.ErrStaleReference,
			lastItem.NamespaceFailoverVersion,
			refVersionedTransition.NamespaceFailoverVersion,
		)
	}
	if idx == len(history)-1 && maxTransitionCount < refVersionedTransition.TransitionCount {
		return fmt.Errorf(
			"%w: state transition count < ref transition count: %v < %v",
			consts.ErrStaleState,
			maxTransitionCount,
			refVersionedTransition.TransitionCount,
		)
	}
	if minTransitionCount > refVersionedTransition.TransitionCount || maxTransitionCount < refVersionedTransition.TransitionCount {
		return fmt.Errorf(
			"%w: ref transition count out of range for version %v: %v not in [%v, %v]",
			consts.ErrStaleReference,
			refVersionedTransition.NamespaceFailoverVersion,
			refVersionedTransition.TransitionCount,
			minTransitionCount,
			maxTransitionCount,
		)
	}
	return nil
}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Retry after replication catches up: GetOrPollWorkflowMutableState handles this by polling the source until the state is current.
  2. Check replication task backlogs on the target cluster and process them in order.
  3. Verify event ordering/sequence handling in the replication stream; missing events should trigger a re-fetch of state from source.
  4. If persistently stale, check for lost replication tasks and trigger a state refresh or history repair from the source cluster.
Defensive patterns

Strategy: retry

Validate before calling

func isStaleState(err error) bool {
    return errors.Is(err, consts.ErrStaleState)
}
// pre-check before applying events
if last := history[len(history)-1]; last.TransitionCount < ref.TransitionCount {
    // state behind: fetch newer state first
}

Try / catch

err := applyBackfillEvents(ctx, state, events)
if errors.Is(err, consts.ErrStaleState) {
    return pollForCurrentMutableState(ctx, workflowKey) // wait for replication to catch up
}
return err

Prevention

When it happens

Trigger: Calling StalenessCheck (via IsStale, GetOrPollWorkflowMutableState, applyBackfillEvents, ReplicateVersionedTransition) where idx == len(history)-1 and maxTransitionCount < refVersionedTransition.TransitionCount — i.e. same failover version but the local state has fewer transitions than the incoming reference.

Common situations: Replication lag within the same failover epoch: target cluster's mutable state hasn't caught up with source transitions; concurrent replication tasks applied out of order; a shard replaying events against a partially-replicated state.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/98cb08d2f14a63b4. Report an issue: GitHub.