{"record":{"id":"101e1786b6ca7dd6","repo":"temporalio/temporal","slug":"task-key-sub-encountered-underflow-self-v-subt","errorCode":null,"errorMessage":"Task key Sub encountered underflow: self: %v, subtrahend: %v","messagePattern":"Task key Sub encountered underflow: self: (.+?), subtrahend: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"service/history/tasks/key.go","lineNumber":102,"sourceCode":"\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)\n\tdifferenceTaskID := k.TaskID - subtrahend.TaskID\n\tif differenceTaskID < 0 {\n\t\tborrow = 1\n\t\tdifferenceTaskID += MaximumKey.TaskID\n\t}\n\n\tfireTime := k.FireTime.UnixNano() - borrow\n\tsubtrahendFireTime := subtrahend.FireTime.UnixNano()\n\tif fireTime < subtrahendFireTime {\n\t\tpanic(fmt.Sprintf(\"Task key Sub encountered underflow: self: %v, subtrahend: %v\", k, subtrahend))\n\t}\n\n\treturn NewKey(\n\t\ttime.Unix(0, fireTime-subtrahendFireTime).UTC(),\n\t\tint64(differenceTaskID),\n\t)\n}\n\nfunc MinKey(this Key, that Key) Key {\n\tif this.CompareTo(that) < 0 {\n\t\treturn this\n\t}\n\treturn that\n}\n\nfunc MaxKey(this Key, that Key) Key {\n\tif this.CompareTo(that) < 0 {\n\t\treturn that","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/temporalio/temporal/blob/bde624efd13fbd3843654058db6d9c716166318b/service/history/tasks/key.go#L84-L120","documentation":"Key.Sub subtracts one key from another to yield the distance (e.g. for backlog metrics or lag computation). After borrowing across the TaskID boundary, if the resulting fireTime is still less than the subtrahend's fireTime, the subtraction underflows and the method panics — meaning 'self' is not greater than or equal to 'subtrahend'.","triggerScenarios":"Calling keyA.Sub(keyB) where keyB >= keyA, e.g. computing lag with a cursor that has moved past the comparison key, or passing arguments in the wrong order (min.Sub(max) instead of max.Sub(min)).","commonSituations":"Backlog/standby lag metric loops after clocks change or cursors reset; comparing keys from different shards or clusters with unsynchronized clocks; refactors that swap the argument order.","solutions":["Check argument order: compute largerKey.Sub(smallerKey); verify self >= subtrahend before calling.","Guard the call site with a comparison and clamp the result to zero when self <= subtrahend.","If clocks jumped backwards (NTP correction), reset the affected cursor/task-min-scheduled-time so keys stay monotonic.","Log both keys at the call site to identify which component produced the out-of-order keys."],"exampleFix":"// before\nlag := maxKey.Sub(minKey) // panics if minKey >= maxKey\n\n// after\nvar lag tasks.Key\nif maxKey.CompareTo(minKey) > 0 {\n    lag = maxKey.Sub(minKey)\n}","handlingStrategy":"validation","validationCode":"if self.CompareTo(subtrahend) <= 0 {\n    return 0 // no lag; skip Sub\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always subtract smaller from larger: maxKey.Sub(minKey)","Clamp lag metrics to zero when cursors cross due to clock changes","Keep task timestamps monotonic; reset cursors after NTP backward jumps"],"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"}