temporalio/temporal · error
%w: ref transition count out of range for version %v: %v not
Error message
%w: ref transition count out of range for version %v: %v not in [%v, %v]
What it means
StalenessCheck verifies that a task/request's imprinted VersionedTransition reference (namespace failover version + transition count) actually exists within the target workflow's stored transition history. This error is returned (wrapping history/consts.ErrStaleReference) when the reference's namespace failover version IS present in the history, but its TransitionCount falls outside the valid range [min+1, max] for that version, meaning the reference points to an impossible or already-superseded state (e.g. after split-brain reconciliation).
Source
Thrown at common/persistence/transitionhistory/transition_history.go:115
)
}
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
}
// transitionHistoryRangeForVersion finds the index and transition count range in the given history for the given version.
func transitionHistoryRangeForVersion(
history []*persistencespb.VersionedTransition,
version int64,
) (idx int, minTransitionCount int64, maxTransitionCount int64) {
prevVersionMaxTransitionCount := int64(-1)
for i, item := range history {View on GitHub (pinned to bde624efd1)
Solutions
- Treat as a terminal ErrStaleReference: drop or discard the stale task/request rather than retrying against this state
- Reload the workflow mutable state from persistence and re-derive the current versioned transition reference before re-attempting
- Verify replication/reconciliation logic is not fabricating transition counts beyond the recorded per-version max
- Check for split-brain (two history shards active) or a sharding/history-rollback incident and reconcile state
Example fix
// before: retrying on any staleness error
callWithState(...)
// after: check terminal stale reference and reload
err := transitionhistory.StalenessCheck(ms.VersionedTransitions, ref)
if errors.Is(err, consts.ErrStaleState) || errors.Is(err, consts.ErrStaleReference) {
ms, err = shards.ReloadWorkflowMutableState(ctx, ref.WorkflowKey)
// handle reload, then retry once with fresh state
} Defensive patterns
Strategy: type-guard
Validate before calling
// before using a ref against a state, pre-check locally
func refInRange(history []*persistencespb.VersionedTransition, ref *persistencespb.VersionedTransition) bool {
if len(history) == 0 || ref == nil { return false }
for i, item := range history {
if item.NamespaceFailoverVersion == ref.NamespaceFailoverVersion {
min := int64(0)
if i > 0 { min = history[i-1].TransitionCount + 1 }
return ref.TransitionCount >= min && ref.TransitionCount <= item.TransitionCount
}
}
return false
} Type guard
func isStaleReference(err error) bool {
return errors.Is(err, consts.ErrStaleReference)
} Try / catch
if err := transitionhistory.StalenessCheck(state.VersionedTransitions, ref); err != nil {
if errors.Is(err, consts.ErrStaleReference) {
// terminal: discard/refetch, do not retry against same state
ms, rerr := reloadMutableState(ctx, key)
...
}
return err
} Prevention
- Always reload mutable state before reusing an imprinted versioned transition reference
- Monitor for split-brain / duplicate shard ownership which produces impossible refs
- Never fabricate transition counts in replication or reconciliation code paths
- Use errors.Is against consts.ErrStaleState vs consts.ErrStaleReference to decide retry vs discard
When it happens
Trigger: Calling StalenessCheck (directly or via IsStale, GetOrPollWorkflowMutableState, applyBackfillEvents, or ReplicateVersionedTransition) with a ref whose NamespaceFailoverVersion matches an entry in the history, but whose TransitionCount is either below the previous version's max+1 (reference predates that version's window) or above that version's recorded max (reference claims more transitions than the state has ever recorded).
Common situations: A duplicate/stale task or replication task referencing transition counts the history node never recorded; a workflow whose mutable state was rebuilt or truncated after failover so its history shrank below the caller's reference; split-brain repair reconciling two nodes with divergent histories; clocks/version skew after namespace failover where a ref carries a version present in history but counts from a divergent branch.
Related errors
- %w: zombie workflow cannot be updated
- ErrStaleReference
- unknown archetypeID: %v
- task queue user data replication failed: %v
- cannot handle replication task of type %v
AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01).
Data as JSON: /api/errors/b4792b1b6fd04874.
Report an issue: GitHub.