A token expiry heuristic misidentifies seconds as milliseconds, producing a 360 billion ms setTimeout. Node.js clamps it to 1ms. The gateway hits 100% CPU and never recovers.
The Bug
Imagine this: you upgrade your OpenClaw gateway, restart it, and within seconds your CPU pegs at 100%. Logs start flooding at 500MB/day. Your agent is completely unresponsive.
The culprit? A setTimeout call with a 360 billion millisecond delay.
OpenClaw #55360 documents one of those bugs that feels like it shouldn't be possible — until you understand how Node.js handles timer overflow.
The Heuristic That Broke
OpenClaw's GitHub Copilot integration needs to refresh tokens before they expire. The token response includes an expires_at field, but different providers return this as either seconds or milliseconds since epoch. The code uses a heuristic to guess which:
if (expiresAt > 10_000_000_000) {
// Must be milliseconds
return expiresAt;
} else {
// Must be seconds, convert
return expiresAt * 1000;
}
This worked fine... until it didn't.
The problem: some Copilot tokens have expires_at values greater than 10 billion in seconds. The current epoch in seconds is around 1.77 billion, and typical token expiries are hours to days ahead — well under 10B. But certain edge cases produce larger values. When they do, the heuristic says "that's milliseconds!" and passes it through unchanged.
The resulting delay: expiresAt - Date.now() ≈ 360 billion milliseconds.
Node.js Timer Overflow
Here's the thing most JavaScript developers don't know: setTimeout uses a 32-bit signed integer internally. The maximum safe value is 2^31 - 1 = 2,147,483,647 ms (about 24.8 days).
When you pass a value larger than that:
TimeoutOverflowWarning: 359994964786 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
Node.js doesn't throw. It doesn't refuse. It silently clamps the timeout to 1 millisecond.
So the refresh callback fires immediately. It computes the same huge delay. Passes it to setTimeout. Node.js clamps it to 1ms again. The callback fires immediately. And again. And again.
100% CPU. Infinite loop. 400 iterations per second.
Why This Is Insidious
- No crash. The process stays alive, burning CPU, generating warnings. Health checks might still pass.
-
No obvious error. You see
TimeoutOverflowWarningin logs, but it's a warning, not an error. Easy to miss in the flood. - Delayed onset. Only triggers with specific token expiry values. Could work fine for months.
- Log explosion. 500MB/day of warning output.
The Fix
Two changes:
Fix the heuristic. Raise the threshold from 10_000_000_000 to 100_000_000_000. Seconds-epoch values won't reach 100B until the year 5138. Milliseconds-epoch values have been above 100B since 1973.
Clamp the timer. Add MAX_SAFE_TIMEOUT = 2_147_483_647 and clamp all setTimeout delays. Defense-in-depth — even if the heuristic fails, you get a 24.8-day timer instead of an infinite loop.
Lessons for Agent Builders
Know your runtime's numeric limits. JavaScript's Number can hold huge values, but the APIs you pass them to often can't. setTimeout has a 32-bit limit.
Heuristics need boundaries, not just thresholds. If your heuristic can be wrong, make sure the consequences of being wrong are bounded.
Warning ≠ benign. In an unattended agent, nobody's reading warnings in real time.
Test with extreme values. If your code handles numbers from external APIs, test with values near Number.MAX_SAFE_INTEGER and just above/below your thresholds.
Found this useful? I write about AI agent infrastructure bugs at oolong-tea-2026.github.io. Follow me on X @realwulong.
Top comments (0)