Does setTimeout(fn, 100) guarantee that fn runs exactly 100 ms later? What does the delay argument actually promise?
answer
- a floor, not an appointment
- the callback is only queued
- nothing preempts running code
- busy loop pushes every timer late
- hosts add clamps on top
basics
~20 ssetTimeout's delay is a minimum wait, not an exact schedule. When it elapses the callback is only queued as a task; it runs after the currently executing code finishes and after any work already queued ahead of it, so it commonly fires late.
solid answer
~50 sNo. The delay is a lower bound: it says "do not run this callback before 100 ms have passed", not "run it at 100 ms". When the delay expires the host does not interrupt anything — it queues the callback as a task. JavaScript runs to completion, so that task can only start once the currently executing function and everything below it on the call stack have returned, and once tasks queued earlier have run. If the current turn spends 500 ms in a synchronous loop, a 100 ms timer fires at roughly 500 ms. Hosts add their own floors on top: browsers clamp deeply nested timers to 4 ms and throttle hidden tabs, and a suspended machine does not run timers at all. So `setTimeout` is a scheduling hint, and you should never treat it as a clock.
code
javascript · 9 linesconst start = Date.now();
setTimeout(() => console.log('timer fired at', Date.now() - start, 'ms'), 100);
const until = Date.now() + 500;
while (Date.now() < until) {} // occupy the single thread
console.log('sync work done at', Date.now() - start, 'ms');
// sync work done at 500 ms
// timer fired at 500 msgo deeper
Be able to say plainly that the delay is a minimum, that the callback is queued rather than run, and that a busy thread makes timers fire late. Knowing clearTimeout cancels a pending timer is expected too.
Explain the mechanism: the timer enqueues a task, run-to-completion means it cannot start until the stack unwinds and the microtask checkpoint drains, and the host may add its own floors. Be ready to predict the output of a blocked-thread example.
Show you treat timer lateness as a production signal. Talk about measuring scheduled-versus-actual firing time, diagnosing long synchronous work from it, and never letting a timer be the authority for a deadline that has real consequences.
Own the design consequence: any correctness that rests on client-side timing is advisory. Argue for absolute deadlines carried in data, server-side enforcement of anything security- or billing-relevant, and explicit budgets for how long a single task may hold the thread.
## What the delay argument means `setTimeout(callback, delay)` asks the host environment to invoke `callback` **no sooner than** `delay` milliseconds from now. Nothing in the contract promises an upper bound. The spec language is deliberately weak: the host is allowed to wait longer for any reason, and in practice it always does wait at least a little longer. Note also that `setTimeout` is not part of ECMAScript at all. The language spec defines the job queue and run-to-completion; `setTimeout` is supplied by the host — the HTML standard for browsers and workers, and its own timer implementation in Node. ## Why the callback is late JavaScript executes one task at a time on a single thread and each task runs **to completion**: nothing preempts a running function. When the delay expires, the timer does not call your function directly. It enqueues a task. That task is picked up only when: 1. the function currently on the call stack (and everything it called) has returned, and 2. the pending microtask checkpoint has been drained, and 3. any tasks queued before yours have already been handled. So the observable delay is `requested delay + however long the loop stays busy`. This is the single most common surprise: ```js const start = Date.now(); setTimeout(() => console.log('timer', Date.now() - start), 100); const until = Date.now() + 500; while (Date.now() < until) {} // block the only thread console.log('sync', Date.now() - start); // sync 500 // timer 500 <- not 100 ``` The timer did not "miss"; it became ready at 100 ms and simply had nowhere to run until 500 ms. ## The other sources of extra delay - **Host floors.** In browsers, once timers are nested more than five levels deep, a requested delay below 4 ms is raised to 4 ms. Node applies a floor of 1 ms to any delay below 1. - **Throttling.** A tab in the background has its timers clamped to roughly once per second, and much less often after the tab has been hidden for several minutes. A sleeping laptop runs no timers. - **Timer resolution and rounding.** The underlying clock has finite granularity, and browsers deliberately coarsen timing to limit side-channel measurement, so sub-millisecond precision is not something you can rely on. - **Queue position.** Other tasks — other expired timers, host-generated work — may be sitting ahead of yours. ## Argument handling The delay is coerced to a number. A missing delay, `undefined`, `NaN`, a non-numeric string, or a negative value all end up treated as 0 (browsers) or 1 (Node) — a timer scheduled for "as soon as possible", not one that runs synchronously and not an error. Extra arguments after the delay are forwarded to the callback: ```js setTimeout((a, b) => console.log(a + b), 0, 2, 3); // logs 5 ``` The return value identifies the pending timer so it can be cancelled with `clearTimeout`. In browsers it is a positive integer; in Node it is a `Timeout` object. Cancelling after the callback has already run is harmless and does nothing. ## Can it ever fire early? Treat the answer as no. The host is required to wait at least the requested delay, and every real source of variance pushes the firing time later, never earlier. Any design that would break if a timer fired 5 ms early is a design that will also break when it fires 200 ms late. ## What to do instead of trusting the delay - Measure elapsed time with timestamps (`Date.now()` for wall clock, `performance.now()` for a monotonic elapsed measure) rather than assuming the timer fired when you asked. - Do not use a timer as an authority for anything that matters — a session expiry, a rate limit, a countdown that must be right. Store the target moment, and compare against it when the callback actually runs. - If a callback is late by more than a few milliseconds, that is usually a diagnostic signal: something is monopolising the thread. Long, unbroken synchronous work is the usual cause. The short version an interviewer wants to hear: `setTimeout` moves work to a **later turn** of the event loop; the number is the earliest that turn may begin, and the busier the loop, the further past it you land.
- If the delay is only a minimum, what does the extra latency actually consist of?Four things stack up: the remainder of the task currently running, the microtask checkpoint drained after it, any tasks already queued ahead of yours, and host-imposed floors such as the browser's 4 ms nesting clamp or background-tab throttling. Clock granularity adds a little more. All of them push the firing time later; none pull it earlier.
- What happens if you pass a negative or non-numeric delay, like setTimeout(fn, -1) or setTimeout(fn, 'soon')?The value is coerced to a number, and anything negative or `NaN` is treated as the minimum. Browsers use 0, Node uses 1. You get a timer scheduled for the next opportunity — the callback still runs asynchronously in a later task, never synchronously, and no error is thrown.
- How would you tell whether a late timer means your code is slow or the host is throttling it?Record `performance.now()` at scheduling time and again inside the callback, and log the difference. Uniform lateness across all timers, in the hundreds of milliseconds or more, points at long synchronous work blocking the loop. Lateness that appears only when a tab is hidden, snapping to roughly one second or one minute, is host throttling.
saying these in an interview costs you the question
- Says setTimeout runs the callback exactly at the requested time
- Thinks setTimeout starts a new thread or runs in parallel
- Believes an expiring timer interrupts code that is currently running
- Claims setTimeout(fn, 0) executes fn immediately and synchronously
- Assumes a late timer means the callback was dropped or missed