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

  1. Guard the caller: only call Prev() when the key is strictly greater than the MinimumKey/zero key.
  2. Initialize cursors with MinimumKey (or the configured inclusive-min key) instead of the zero Key and stop iterating at the boundary.
  3. Clamp the cursor to tasks.MinimumKey when the computed predecessor would underflow.
  4. 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

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


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