temporalio/temporal · error
Key encountered negative underflow
Error message
Key encountered negative underflow
What it means
Key.Prev computes the lexicographic predecessor of a task key. When TaskID is 0 it rolls back to the previous nanosecond with MaxInt64 as TaskID; if both TaskID and FireTime.UnixNano() are 0 (the epoch-zero key), there is no predecessor and the method panics to avoid negative underflow of the underlying int64 time representation.
Source
Thrown at service/history/tasks/key.go:74
func (left Key) CompareTo(right Key) int {
if left.FireTime.Before(right.FireTime) {
return -1
} else if left.FireTime.After(right.FireTime) {
return 1
}
if left.TaskID < right.TaskID {
return -1
} else if left.TaskID > right.TaskID {
return 1
}
return 0
}
func (k Key) Prev() Key {
if k.TaskID == 0 {
if k.FireTime.UnixNano() == 0 {
panic("Key encountered negative underflow")
}
return NewKey(k.FireTime.Add(-time.Nanosecond), math.MaxInt64)
}
return NewKey(k.FireTime, k.TaskID-1)
}
func (k Key) Next() Key {
if k.TaskID == math.MaxInt64 {
if k.FireTime.UnixNano() == math.MaxInt64 {
panic("Key encountered positive overflow")
}
return NewKey(k.FireTime.Add(time.Nanosecond), 0)
}
return NewKey(k.FireTime, k.TaskID+1)
}
func (k Key) Sub(subtrahend Key) Key {
borrow := int64(0)View on GitHub (pinned to bde624efd1)
Solutions
- Guard the caller: only call Prev() when the key is strictly greater than the MinimumKey/zero key.
- Initialize cursors with MinimumKey (or the configured inclusive-min key) instead of the zero Key and stop iterating at the boundary.
- Clamp the cursor to tasks.MinimumKey when the computed predecessor would underflow.
- Add a unit test for the boundary key to catch the regression in the calling loop.
Example fix
// before
nextMinKey := currentMinKey.Prev()
// after
var nextMinKey tasks.Key
if currentMinKey != tasks.MinimumKey {
nextMinKey = currentMinKey.Prev()
} else {
nextMinKey = tasks.MinimumKey
} Defensive patterns
Strategy: validation
Validate before calling
if k == (tasks.Key{}) || k.CompareTo(tasks.MinimumKey) <= 0 {
return tasks.MinimumKey // do not call Prev
} Prevention
- Never seed cursors with the zero Key; use tasks.MinimumKey as the lower bound
- Stop backward iteration when the cursor reaches MinimumKey
- Add boundary tests for Key.Prev at TaskID=0 and FireTime=0
When it happens
Trigger: Calling Key.Prev() on the zero key NewKey(time.Unix(0,0), 0) — occurs when a task-scanner/standby-task processing loop backs up its cursor past the very first possible key, e.g. initializing a minimum-key cursor from a zero value and calling Prev.
Common situations: Bootstrapping standby/standby-cluster task processing where the starting cursor is the zero Key; unit tests constructing the zero Key and iterating backwards; persistence returning empty/zero keys that are then decremented.
Related errors
- Key encountered positive overflow
- Task key Sub encountered underflow: self: %v, subtrahend: %v
- Unknown category type: %v
- Found key with non-zero pending task count but has no corres
- unknown task predicate type: %T
AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01).
Data as JSON: /api/errors/94dee720c2e8b749.
Report an issue: GitHub.