skip to content

What happens when you call setTimeout(fn, 2 ** 31) — a delay of about 25 days — and why?

level: middleimportance: nice to knowfreq 18%

answer

  1. 32-bit signed, not a 64-bit float
  2. about 24.8 days is the ceiling
  3. overflow fires early, not late
  4. Node warns; browsers stay quiet
  5. store the deadline, not the offset

basics

~20 s

The delay is held in a 32-bit signed integer, so anything above 2147483647 ms (about 24.8 days) overflows. Browsers then treat it as zero and fire almost immediately; Node clamps it to 1 ms and prints a TimeoutOverflowWarning.

solid answer

~50 s

It fires almost at once, which is the opposite of what the code says. Timer delays are stored in a 32-bit signed integer, so the largest value that survives is 2147483647 ms — roughly 24.8 days. Pass more and the value wraps to a negative number, which the host then clamps up to the minimum, so the callback runs on the next turn of the loop. Node makes it visible with a `TimeoutOverflowWarning` and sets the delay to 1 ms; browsers just fire. The bug shows up in real code that computes a delay as `target - Date.now()` for a far-future target, or that multiplies days into milliseconds. The fix is not a bigger number: check against the limit and either re-arm the timer in chunks, or store the absolute deadline and compare `Date.now()` against it when you next get a chance to look.

code

javascript · 9 lines
javascript
const MAX_DELAY = 2147483647; // 2 ** 31 - 1, about 24.8 days

function runAt(deadline, fn) {
  const remaining = deadline - Date.now();
  if (remaining <= 0) return fn();
  setTimeout(() => runAt(deadline, fn), Math.min(remaining, MAX_DELAY));
}

runAt(Date.now() + 40 * 24 * 60 * 60 * 1000, () => console.log('40 days later'));

go deeper

for a junior

Know there is an upper limit of about 24.8 days on a timer delay, and that exceeding it makes the callback fire straight away rather than late. Recognise the Node warning text if you see it.

for a middle

Explain the mechanism — a 32-bit signed store, wraparound to a negative value, then clamping up to the host minimum — and note that browsers fail silently while Node emits TimeoutOverflowWarning.

for a senior

Show how the value gets there in practice: computed offsets, config-driven durations, unbounded backoff. Talk about validating computed delays and about re-arming in chunks from a recomputed remaining time.

for a principal

Own the rule that long-horizon scheduling does not belong in an in-process timer. Argue for durable deadlines attached to the data, evaluated by something that survives a reload or restart, with client timers treated as an optimisation only.

## The surprise ```js setTimeout(() => console.log('a month from now?'), 2 ** 31); // logs almost immediately ``` A delay that reads as "in about 25 days" behaves as "right now". This is one of the few genuine landmines in the timer API, because it fails in the least safe direction: the callback runs far too early rather than not at all. ## Why 2147483647 Timer delays are stored as a signed 32-bit integer, whose maximum is 2^31 − 1 = 2147483647. In milliseconds that is about 24 days, 20 hours, 31 minutes. Ask for one millisecond more and the value no longer fits; it wraps into the negative range. Negative delays are then clamped to the host minimum — 0 in browsers, 1 in Node — so the timer becomes an immediate one. Node surfaces this rather than hiding it: ``` (node) TimeoutOverflowWarning: 2147483648 does not fit into a 32-bit signed integer. Timeout duration was set to 1. ``` Browsers generally fire without complaint, so in a page the only evidence is the callback running at the wrong time. ## Where it actually bites Nobody writes `2 ** 31` on purpose. The value arrives computed: - `const ms = days * 24 * 60 * 60 * 1000;` with `days` set to 30 by configuration. - `const ms = expiresAt - Date.now();` where `expiresAt` is a far-future timestamp, or where a bad parse made it enormous. - A retry backoff that doubles without a ceiling and eventually crosses the limit. The unit test with a 5-second delay passes; the production config with a 30-day token lifetime fires the "session expired" handler the instant the page loads. The same limit applies to `setInterval`. ## Guarding it The correct posture is to treat any delay you did not literally type as untrusted, and to design around the absolute moment rather than the offset. Two workable shapes: **Re-arm in chunks.** Sleep for at most the maximum, then check whether you have arrived and re-arm if not. ```js const MAX_DELAY = 2147483647; function runAt(deadline, fn) { const remaining = deadline - Date.now(); if (remaining <= 0) return fn(); setTimeout(() => runAt(deadline, fn), Math.min(remaining, MAX_DELAY)); } ``` Note that this also self-corrects: each hop recomputes the remaining time from the clock, so it does not accumulate error, and it re-reads the deadline after the machine has been asleep. **Do not use a timer at all.** For anything measured in days, a long-lived timer is the wrong mechanism regardless of the 32-bit limit: a browser tab will be closed, a page reloaded, a process restarted long before it fires. Store the deadline where it survives — in the record the work belongs to — and check it when something happens. A timer is for "soon"; a timestamp is for "eventually". ## Related clamping, kept straight It is easy to file every timer surprise under one heading. Three distinct rules are at work: - **The upper limit** described here: above ~24.8 days the value overflows and the timer becomes immediate. - **The lower floor:** browsers raise a sub-4 ms delay to 4 ms once a timer chain nests more than five deep; Node raises anything below 1 to 1. - **Throttling:** hidden browser tabs have timers clamped to roughly once per second, and much less often after several minutes in the background. Only the first one makes a timer fire *earlier* than requested; the other two make it later. If you can name which of the three you are looking at from the symptom, you understand the API well enough for any interview that asks.

  • How would this bug typically reach production without being caught in review?
    The literal is rarely in the source. The delay is computed — days times 3600000, or `expiresAt - Date.now()` for a far-future timestamp — so it only crosses the limit under a particular configuration or data value. Tests use short delays and pass. In a browser nothing is logged, so the only symptom is a handler firing immediately in production.
  • What is the right way to schedule work weeks away?
    Do not hold it in a timer at all. Persist the absolute deadline alongside the data it concerns, and evaluate it when the process next has reason to look — on load, on a periodic sweep, or server-side. A tab is closed and a process restarts long before a 25-day timer would fire, so the timer is unreliable independently of the 32-bit limit.
  • Does the same limit apply to setInterval?
    Yes — the period is stored the same way, so an interval above 2147483647 ms overflows identically and becomes an immediate, repeating timer. That failure is nastier than the setTimeout one, because instead of a single early callback you get a tight repeating loop firing at the host minimum.

saying these in an interview costs you the question

  • Assumes any millisecond value works because JavaScript numbers are doubles
  • Thinks an over-large delay simply never fires
  • Expects a RangeError to be thrown for an out-of-range delay
  • Fixes it by passing an even larger number
  • Believes the limit applies to setTimeout but not setInterval

context