{"record":{"id":"94dee720c2e8b749","repo":"temporalio/temporal","slug":"key-encountered-negative-underflow","errorCode":null,"errorMessage":"Key encountered negative underflow","messagePattern":"Key encountered negative underflow","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"service/history/tasks/key.go","lineNumber":74,"sourceCode":"func (left Key) CompareTo(right Key) int {\n\tif left.FireTime.Before(right.FireTime) {\n\t\treturn -1\n\t} else if left.FireTime.After(right.FireTime) {\n\t\treturn 1\n\t}\n\n\tif left.TaskID < right.TaskID {\n\t\treturn -1\n\t} else if left.TaskID > right.TaskID {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\nfunc (k Key) Prev() Key {\n\tif k.TaskID == 0 {\n\t\tif k.FireTime.UnixNano() == 0 {\n\t\t\tpanic(\"Key encountered negative underflow\")\n\t\t}\n\t\treturn NewKey(k.FireTime.Add(-time.Nanosecond), math.MaxInt64)\n\t}\n\treturn NewKey(k.FireTime, k.TaskID-1)\n}\n\nfunc (k Key) Next() Key {\n\tif k.TaskID == math.MaxInt64 {\n\t\tif k.FireTime.UnixNano() == math.MaxInt64 {\n\t\t\tpanic(\"Key encountered positive overflow\")\n\t\t}\n\t\treturn NewKey(k.FireTime.Add(time.Nanosecond), 0)\n\t}\n\treturn NewKey(k.FireTime, k.TaskID+1)\n}\n\nfunc (k Key) Sub(subtrahend Key) Key {\n\tborrow := int64(0)","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/temporalio/temporal/blob/bde624efd13fbd3843654058db6d9c716166318b/service/history/tasks/key.go#L56-L92","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nnextMinKey := currentMinKey.Prev()\n\n// after\nvar nextMinKey tasks.Key\nif currentMinKey != tasks.MinimumKey {\n    nextMinKey = currentMinKey.Prev()\n} else {\n    nextMinKey = tasks.MinimumKey\n}","handlingStrategy":"validation","validationCode":"if k == (tasks.Key{}) || k.CompareTo(tasks.MinimumKey) <= 0 {\n    return tasks.MinimumKey // do not call Prev\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["task-key","panic","integer-overflow","history-service"],"backgroundTag":"task-key-underflow","analyzedSha":"bde624efd13fbd3843654058db6d9c716166318b","analyzedAt":"2026-09-01T07:18:39.080Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}