Back to Blog

Language: English

I Passed 720 Hours to setInterval and Got 1 Millisecond

Passing thirty days' worth of milliseconds to setInterval exceeded Node.js's TIMEOUT_MAX timer limit, and the delay silently became 1 millisecond. This post follows the process of spotting the anomaly from the log's abnormal frequency and landing on a fix that clamps at the boundary.

While I was working on performance improvements for our annotation platform, I noticed something odd in Cloud Logging. A log saying the email-to-Slack-ID lookup table had been loaded kept streaming past, again and again. That table was designed to load once at startup and simply reload every 30 days after that. A log that was supposed to appear once every 30 days kept showing up while I watched for just a few minutes.

The environment variable and setInterval

The reload interval lived in an environment variable.

const reloadIntervalHours = Number(
  config.get("EMAIL_SLACK_MAPPING_RELOAD_INTERVAL_HOURS"),
); // 720

const intervalMs = reloadIntervalHours * 60 * 60 * 1000;

setInterval(() => {
  reloadEmailSlackMapping();
}, intervalMs);

The value was 720 hours—30 days. The second argument of setInterval is in milliseconds, so we convert hours to milliseconds before passing it along. 720 × 60 × 60 × 1000 comes to 2,592,000,000.

The arithmetic is correct. The config value is what we intended. On paper, not a single thing is wrong.

Node.js’s timer upper bound

The cause was that the number we passed exceeded what a Node.js timer can represent.

// lib/internal/timers.js (Node.js v22.22.0)
const TIMEOUT_MAX = 2 ** 31 - 1;

after *= 1;
if (!(after >= 1 && after <= TIMEOUT_MAX)) {
  if (after > TIMEOUT_MAX) {
    process.emitWarning(/* ... */, 'TimeoutOverflowWarning');
  }
  after = 1;
}

this._repeat = isRepeat ? after : null;

TIMEOUT_MAX is 2,147,483,647. The 2,592,000,000 we passed exceeds that. When it does, Node.js emits a warning and replaces the delay with the minimum value of 1.

That 1 lands in this._repeat, so the repeat interval becomes 1 millisecond too. The recurring job we wrote expecting 30 days was now recurring every millisecond. The longer you configure it, the shorter it degenerates.

Source: https://github.com/nodejs/node/blob/v22.22.0/lib/internal/timers.js

The abnormal firing count

This kind of bug doesn’t get caught in code review. Both 720 and the conversion formula are correct. The only broken spot is the boundary where the application’s computed number meets Node.js’s internal representation, and that boundary appears nowhere in the formula itself.

The first thing that caught me was the same message stacking up at an unnatural rate in Cloud Logging. Say “performance improvement” and you want to start by measuring the slow parts. But problems of this kind surface in logs as an abnormal count.

My fix touched only the boundary where the value is handed to Node.js. The periodic-reload design stayed as it was; I made sure the timer’s constraint is satisfied at that boundary. Since the unit conversion can exceed the constraint, whatever exceeds it has no option but to be clamped right there.

The 24.8-day boundary

Anywhere that passes a value converted from hours or days into setInterval or setTimeout can step on this. 2,147,483,647 milliseconds is roughly 24.8 days. Every design that passes an interval longer than 25 days directly in milliseconds turns into 1 millisecond.

Daily or weekly intervals are safe. The moment you try to express a monthly interval with a single timer, you’re over the line. TimeoutOverflowWarning does get emitted, but it isn’t guaranteed to land anywhere a human reads it. This time, too, I only noticed because the reload’s own logs were so frequent.


Based on material presented at the results presentation on July 31, 2026.