Timers
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with the
differences of Node.js 24 "Krypton" (24.21.0, Active LTS) and Node.js 22 "Jod" (22.23.3, Maintenance) called out; the stability levels
and version notes were checked against the official documentation on 2026-10-03, and every example below was run on all three lines
unless the text says otherwise. A timer asks the runtime to call a function later. In Node.js the timer functions are globals, so no
import is needed, and they are built around the event loop that is explained in
The Event Loop: a timer callback runs in the timers phase, setImmediate() callbacks run in the
check phase, and neither is a promise of an exact time. The Timers module (setTimeout(), setInterval(), setImmediate(), the
clear* functions, and the Timeout and Immediate classes) is Stability 2 (Stable). The node:timers/promises API is Stable as well
(it graduated from experimental in v16.0.0), with two exceptions: timersPromises.scheduler.wait() and
timersPromises.scheduler.yield() are Stability 1 (Experimental), because they implement a draft web standard. timeout.close() is
Stability 3 (Legacy), and timeout[Symbol.dispose]() and immediate[Symbol.dispose]() are Stable since v24.2.0 but are still
documented as Experimental on Node.js 22. The JavaScript-language view of the same functions is in
Standard Library: Console, URL & Timers, and
the promise vocabulary used below is explained in Callbacks, Promises & async/await.
The running scenario is the _Bookshelf application that grows across this section.
The timer functions at a glance
| API | Stability | Notes |
|---|---|---|
|
2 Stable |
Runs |
|
2 Stable |
Runs |
|
2 Stable |
Runs |
|
2 Stable |
Cancel a timer. The first two accept a |
|
2 Stable |
Control whether a timer keeps the process alive. |
|
2 Stable |
Restarts the countdown of the same object. Added in v10.2.0. |
|
2 Stable on 24.2+ and 26, Experimental on 22 |
Cancel the timer, which lets a timer be owned by a |
|
3 Legacy |
Use |
|
2 Stable |
Promise and async-iterable variants. Added in v15.0.0 ( |
|
1 Experimental |
Added in v17.3.0 and v16.14.0. Equivalent to |
setTimeout, setInterval and setImmediate
setTimeout() schedules a one-time call, setInterval() schedules a call that repeats until it is cancelled, and setImmediate()
schedules a call for the check phase of the current loop iteration, which is why it takes no delay. All three accept extra arguments
that are passed to the callback, which avoids creating a closure just to carry values (the Learn article shows the same
setTimeout(myFunction, 2000, firstParam, secondParam) form). setTimeout() and setInterval() return a Timeout object and
setImmediate() returns an Immediate; both can be passed to the matching clear* function, and a cancelled timer never runs. Passing something that is not a function throws a TypeError
with code ERR_INVALID_ARG_TYPE (since v18.0.0; earlier lines used ERR_INVALID_CALLBACK).
const started = performance.now();
const elapsed = () => `${Math.round(performance.now() - started)} ms`;
// setTimeout: one-shot, extra arguments are passed to the callback
const timeout = setTimeout((title, year) => {
console.log(`timeout fired at ~${elapsed()}: ${title} (${year})`);
}, 50, 'Dune', 1965);
// setInterval: repeats until cleared
let ticks = 0;
const interval = setInterval(() => {
ticks += 1;
console.log(`tick ${ticks} at ~${elapsed()}`);
if (ticks === 3) clearInterval(interval);
}, 20);
// setImmediate: runs after the current poll phase, no delay argument
const immediate = setImmediate(() => console.log('immediate'));
// A timer that is cancelled before it fires never runs
const cancelled = setTimeout(() => console.log('never printed'), 10);
clearTimeout(cancelled);
console.log(timeout.constructor.name, immediate.constructor.name);
console.log('scheduled');
Timeout Immediate
scheduled
immediate
tick 1 at ~21 ms
tick 2 at ~42 ms
timeout fired at ~51 ms: Dune (1965)
tick 3 at ~63 ms
The Learn article also describes a "zero delay" setTimeout(): the callback is not run synchronously, it runs as soon as possible
after the current code has finished. In Node.js a delay of 0 is not literally zero, because the minimum is 1 ms and smaller values
are replaced by 1 (see Timer accuracy, drift and limits).
Recursive setTimeout instead of setInterval
setInterval() fires on a fixed schedule whether or not the previous callback has finished. With asynchronous work in the callback
(a request, a database query) the runs can overlap and pile up. The pattern the Learn article calls recursive setTimeout
schedules the next run only after the current one has completed, so runs never overlap and there is always a full delay between the
end of one run and the start of the next. The async iterator of node:timers/promises (below) avoids the overlap too, but it does not
guarantee that pause: the underlying interval keeps ticking while the loop body works, and a tick that has already elapsed is delivered
as soon as the body asks for the next value, so when the work takes longer than the interval the iterations run back to back.
import { setInterval as every } from 'node:timers/promises';
const work = () => new Promise((r) => setTimeout(r, 60)); // slow work: 60 ms
const since = (t) => `~${Math.round(performance.now() - t)} ms`;
// 1. Async-iterable interval: no overlap, but ticks that elapsed during the work
// are delivered at once, so the next iteration starts with no pause.
let t = performance.now();
let i = 0;
for await (const _ of every(20)) {
i += 1;
console.log(`iteration ${i} starts at ${since(t)}`);
await work();
if (i === 3) break;
}
// 2. Recursive setTimeout: the next run is scheduled only after the work finished,
// so there is always a full 20 ms pause between runs.
t = performance.now();
let k = 0;
await new Promise((done) => {
const run = async () => {
k += 1;
console.log(`run ${k} starts at ${since(t)}`);
await work();
if (k === 3) return done();
setTimeout(run, 20);
};
setTimeout(run, 20);
});
// 3. Classic setInterval: keeps firing while the previous work is still pending
t = performance.now();
let j = 0;
const id = setInterval(async () => {
j += 1;
console.log(`callback ${j} starts at ${since(t)}`);
if (j === 5) clearInterval(id); // stop scheduling; callbacks already started still finish
await work();
}, 20);
iteration 1 starts at ~21 ms
iteration 2 starts at ~82 ms
iteration 3 starts at ~144 ms
run 1 starts at ~21 ms
run 2 starts at ~104 ms
run 3 starts at ~186 ms
callback 1 starts at ~20 ms
callback 2 starts at ~42 ms
callback 3 starts at ~62 ms
callback 4 starts at ~82 ms
callback 5 starts at ~103 ms
The three patterns differ in what happens after 60 ms of work with a 20 ms period. The for await loop never runs two iterations at once,
but each iteration starts about 62 ms after the previous one, that is, right after the work finished: the ticks that fell due during the
work were waiting, so there is no 20 ms gap. The recursive setTimeout() starts each run about 82 ms after the previous one (60 ms of work
plus the full 20 ms delay), which is the pattern to use when a pause between runs matters, for example to avoid hammering an API that is
already slow. The setInterval() callbacks start every 20 ms regardless, so up to three of them are in flight at the same time.
setTimeout(0) versus setImmediate
Both setTimeout(fn, 0) and setImmediate(fn) mean "as soon as possible", and the difference is the loop phase they belong to.
From the main module the order is not deterministic, because it depends on how long the process took to start and on the 1 ms
timer resolution: the example below printed the timeout first on the Node.js 24 and 22 runs and the immediate first on most Node.js 26 runs, and both orders were observed
across repeated runs of Node.js 26. Inside an I/O callback the order is fixed: the loop is already past the timers phase, so the
immediate always runs first. The full phase-by-phase explanation, including process.nextTick() (Stability 3, Legacy) and
queueMicrotask(), is in The Event Loop.
import { readFile } from 'node:fs';
setTimeout(() => console.log('main: timeout'), 0);
setImmediate(() => console.log('main: immediate'));
readFile(import.meta.filename, () => {
setTimeout(() => console.log('io: timeout'), 0);
setImmediate(() => console.log('io: immediate'));
});
main: immediate
main: timeout
io: immediate
io: timeout
ref, unref, hasRef and refresh
By default every active Timeout and Immediate is ref’ed: the process does not exit while it is pending. An interval that is
never cleared therefore keeps a script running forever. unref() tells the loop that the timer alone is not a reason to stay
alive, which is what you want for housekeeping such as a heartbeat, a metrics flush or a cache sweeper that must not delay shutdown.
ref() reverts it, hasRef() reports the current state, and calling ref() or unref() repeatedly has no extra effect. All
three exist on both Timeout and Immediate and return the object, so they chain. timeout.refresh() resets the start time to now
and reschedules the callback for the original duration, without allocating a new object; calling it on a timer that has already
fired re-arms it, which makes it a cheap idle-timeout primitive (reset the timer on every sign of activity).
const heartbeat = setInterval(() => console.log('heartbeat'), 30);
heartbeat.unref();
console.log('hasRef after unref:', heartbeat.hasRef());
const work = setTimeout(() => console.log('work done'), 100);
console.log('hasRef of work:', work.hasRef());
// refresh() restarts the countdown of the same Timeout object
const idle = setTimeout(() => console.log('idle timeout reached'), 80);
setTimeout(() => idle.refresh(), 50); // pushes the idle deadline to ~130 ms
const t0 = performance.now();
idle.ref();
process.on('exit', () => console.log(`exit after ~${Math.round(performance.now() - t0)} ms`));
hasRef after unref: false
hasRef of work: true
heartbeat
heartbeat
heartbeat
work done
heartbeat
idle timeout reached
exit after ~132 ms
The process ends right after the last ref’ed timer (the refreshed idle timeout, due at about 130 ms, not at 80 ms), even though the
heartbeat interval would still be pending. The Timeout also has a Symbol.toPrimitive method (added in v14.9.0 and v12.19.0)
that returns a number, so clearTimeout(+timeout) works, which mirrors the browser where setTimeout() returns a number; the
number is only valid in the thread that created the timer. Because clearTimeout() and clearInterval() are interchangeable in
Node.js, either can cancel either kind of timer, but use the matching one for readability.
Cancelling with Symbol.dispose and using
Both timer classes have a [Symbol.dispose]() method that cancels the timer, equivalent to clearTimeout() or clearImmediate().
It makes a timer usable with the using declaration of explicit resource management, which cancels it when the block ends. The
method itself exists on all three lines (it was added in v20.5.0 and v18.18.0), but the using syntax is a SyntaxError on
Node.js 22 (the V8 12.4 that Node.js 22 ships does not parse it), so on 22 call the method directly.
The topic is covered with await using and AbortSignal in
Async Context & Cancellation.
const timeout = setTimeout(() => console.log('timeout ran'), 20);
const immediate = setImmediate(() => console.log('immediate ran'));
timeout[Symbol.dispose]();
immediate[Symbol.dispose]();
console.log('both cancelled, typeof:', typeof timeout[Symbol.dispose]);
{
using t = setTimeout(() => console.log('never runs: disposed at block end'), 20);
}
both cancelled, typeof: function
node:timers/promises
node:timers/promises offers setTimeout(), setImmediate() and setInterval() that return promises, so a delay can be
await`ed instead of nested in a callback. It is a separate module, imported from `node:timers/promises (a built-in, so the
node: prefix is the canonical spelling). Each function takes an optional value that the promise resolves with and an options
object with two fields: signal, an AbortSignal that cancels the wait and rejects the promise with an AbortError
(err.code is ABORT_ERR), and ref, which when false lets the process exit while the promise is pending. Note that the argument order differs from the callback API
(setTimeout(delay, value, options), with no callback).
import { setTimeout as sleep, setImmediate as tick, setInterval as every, scheduler } from 'node:timers/promises';
// 1. A promise that resolves with a value after a delay
const value = await sleep(50, 'dune');
console.log('slept, value =', value);
// 2. Cancellation with an AbortSignal rejects with AbortError
const ac = new AbortController();
const pending = sleep(10_000, 'never', { signal: ac.signal });
setTimeout(() => ac.abort(), 20);
try {
await pending;
} catch (err) {
console.log(err.name, err.code);
}
// 3. AbortSignal.timeout() as a deadline for the wait
try {
await sleep(10_000, undefined, { signal: AbortSignal.timeout(30) });
} catch (err) {
console.log(err.name, err.code);
}
// 4. Async-iterable interval: backpressure-friendly polling, stopped with break
let n = 0;
for await (const startedAt of every(25, Date.now())) {
n += 1;
console.log(`poll ${n} after ~${Math.round((Date.now() - startedAt) / 25) * 25} ms`);
if (n === 3) break;
}
// 5. scheduler.wait() and scheduler.yield()
await scheduler.wait(20);
await scheduler.yield();
await tick();
console.log('done');
slept, value = dune
AbortError ABORT_ERR
AbortError ABORT_ERR
poll 1 after ~25 ms
poll 2 after ~50 ms
poll 3 after ~75 ms
done
setInterval() from node:timers/promises returns an async iterator, so it is consumed with for await and stopped with break
(which cleans up the underlying timer) or with a signal. The documentation points out that with ref: true you must call next()
(explicitly or through for await) to keep the event loop alive. scheduler.wait(delay, options) is the same as
timersPromises.setTimeout(delay, undefined, options) and scheduler.yield() is the same as timersPromises.setImmediate();
yield() is the idiomatic way to give other work a turn while splitting a long synchronous loop into chunks
(see Don’t Block the Event Loop). Both scheduler functions implement the
Scheduling APIs draft and are Stability 1 (Experimental), so they may change, whereas the
three timer functions are Stable. The cancellation topic goes further in
Async Context & Cancellation.
Source: Node.js API: Timers Promises API
ref: false and the unsettled top-level await
A pending ref: false promise does not keep the process alive. In an ES module with a top-level await this means the module
never finishes, the loop empties, and Node.js exits with code 13 and prints a warning about an unsettled top-level await.
import { setTimeout as sleep } from 'node:timers/promises';
// With ref: false the pending promise does not keep the process alive,
// so Node.js exits before the promise ever settles.
process.on('exit', (code) => console.log('exit code', code));
await sleep(60_000, undefined, { ref: false });
console.log('unreachable');
Warning: Detected unsettled top-level await at file:///.../refopt.mjs:6
await sleep(60_000, undefined, { ref: false });
^
exit code 13
A Bookshelf poller with a deadline
Timers, AbortSignal.timeout() and AbortSignal.any() compose into a bounded retry loop. The AbortSignal passed to the sleep
means that when the deadline (or the caller) aborts, the pending timer is cleared and no timer is left behind.
import { setTimeout as sleep } from 'node:timers/promises';
// Bookshelf: poll a catalogue source until it is ready, with a per-attempt delay,
// an overall deadline, and cancellation. No timer is left behind after an abort.
let attempts = 0;
async function fetchStatus() { // stand-in for a real request
attempts += 1;
return attempts >= 4 ? 'ready' : 'pending';
}
export async function waitUntilReady({ everyMs = 25, timeoutMs = 1_000, signal } = {}) {
const deadline = AbortSignal.any([AbortSignal.timeout(timeoutMs), ...(signal ? [signal] : [])]);
while (true) {
if (await fetchStatus() === 'ready') return attempts;
await sleep(everyMs, undefined, { signal: deadline }); // rejects with AbortError when the deadline fires
}
}
console.log('ready after', await waitUntilReady(), 'attempts');
attempts = -1_000_000; // never ready
try {
await waitUntilReady({ everyMs: 20, timeoutMs: 100 });
} catch (err) {
console.log('gave up:', err.name);
}
ready after 4 attempts
gave up: AbortError
Timer accuracy, drift and limits
The setTimeout() documentation is explicit: the callback "will likely not be invoked in precisely delay milliseconds", Node.js
makes no guarantee about exact timing or about the ordering of callbacks, and the callback is called as close to the requested time
as possible. These are the practical consequences.
| Aspect | Behaviour |
|---|---|
Minimum delay |
When |
Lateness |
A timer fires when the timers phase finds it due, so it is normally late by the time the loop spent elsewhere: running other callbacks, a long synchronous function, or the OS scheduler. Measured lateness of an idle process was around one millisecond. |
A blocked loop |
Synchronous work delays every timer. A timer due at 10 ms ran at 100 ms after a 100 ms busy loop. Measure this with |
Interval drift |
|
Maximum delay |
2147483647 ms, which is 2 to the power of 31 minus 1, about 24.8 days. Larger delays are not honoured: Node.js emits a |
Worker threads |
A |
// 1. Timers are best effort: measure how late they actually fire
function lateness(delay) {
return new Promise((resolve) => {
const t0 = performance.now();
setTimeout(() => resolve(performance.now() - t0 - delay), delay);
});
}
console.log('lateness of a 0 ms timer :', (await lateness(0)).toFixed(2), 'ms (delay 0 is clamped to 1)');
console.log('lateness of a 10 ms timer:', (await lateness(10)).toFixed(2), 'ms');
// 2. A busy main thread delays every timer
const t0 = performance.now();
setTimeout(() => console.log('timer due at 10 ms ran at', Math.round(performance.now() - t0), 'ms'), 10);
while (performance.now() - t0 < 100); // block for 100 ms
// 3. Drift of setInterval over 20 ticks of 10 ms
let ticks = 0;
const start = performance.now();
const id = setInterval(() => {
ticks += 1;
if (ticks === 20) {
clearInterval(id);
console.log('20 ticks of 10 ms took', Math.round(performance.now() - start), 'ms');
}
}, 10);
lateness of a 0 ms timer : 1.28 ms (delay 0 is clamped to 1)
lateness of a 10 ms timer: 1.09 ms
timer due at 10 ms ran at 100 ms
20 ticks of 10 ms took 214 ms
Source: Node.js API: setTimeout
Delays beyond the 32-bit limit
Because the delay is stored in a 32-bit signed integer, a longer wait must be built from several shorter ones. On all three lines the overflow case below
emits TimeoutOverflowWarning and the callback runs after about 1 ms. On Node.js 24 and 26, NaN and negative delays also emit a
TimeoutNaNWarning and a TimeoutNegativeWarning (observed in these runs; these warnings are not listed in the timers
documentation), and on Node.js 22 they are silently treated as 1.
const MAX = 2 ** 31 - 1; // 2147483647 ms, about 24.8 days
console.log('largest delay:', MAX, 'ms =', (MAX / 86_400_000).toFixed(1), 'days');
process.on('warning', (w) => console.log(`warning: ${w.name}: ${w.message}`));
const t0 = performance.now();
// Larger than 2^31-1: the delay is reset to 1 ms and the callback runs almost at once
setTimeout(() => console.log('overflow timer ran after', Math.round(performance.now() - t0), 'ms'), MAX + 1);
// Fractions are truncated; NaN and negative delays become 1
setTimeout(() => console.log('fraction ran'), 10.9);
setTimeout(() => console.log('NaN ran'), NaN);
setTimeout(() => console.log('negative ran'), -5);
largest delay: 2147483647 ms = 24.9 days
warning: TimeoutOverflowWarning: 2147483648 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
warning: TimeoutNaNWarning: NaN is not a number.
Timeout duration was set to 1.
warning: TimeoutNegativeWarning: -5 is a negative number.
Timeout duration was set to 1.
overflow timer ran after 0 ms
NaN ran
negative ran
fraction ran
A wait of weeks or months is better handled by storing a deadline (in a database or a queue) than by one process-lifetime timer; a long timer is lost on every restart. For in-process use, re-arm timers no longer than the limit until the deadline has passed.
import { setTimeout as sleep } from 'node:timers/promises';
const MAX_DELAY = 2 ** 31 - 1;
// Wait for any duration by re-arming timers no longer than maxDelay until the deadline passes
export async function sleepLong(ms, { maxDelay = MAX_DELAY, signal } = {}) {
const deadline = Date.now() + ms;
let rounds = 0;
for (let left = ms; left > 0; left = deadline - Date.now()) {
rounds += 1;
await sleep(Math.min(left, maxDelay), undefined, { signal });
}
return rounds;
}
// Demonstrated with a tiny cap so that the loop is visible
const rounds = await sleepLong(130, { maxDelay: 40 });
console.log(`waited 130 ms in ${rounds} rounds of at most 40 ms`);
waited 130 ms in 4 rounds of at most 40 ms
Scheduling that must survive restarts, run on a calendar or fan out across processes belongs to a job scheduler or queue, covered in Background Jobs, Scheduling & Queues.
Source: Node.js API: setTimeout
Testing code that uses timers
Waiting for real time makes tests slow and flaky. The built-in test runner can replace timers with a mock whose clock only moves
when the test says so: mock.timers.enable() (or t.mock.timers.enable(), which is restored automatically when the test ends)
takes the list of APIs to mock, and tick() advances the clock. The mock can also fake Date. The test runner is covered in
Testing with the Node.js Test Runner and
Mocking, Snapshots & Coverage.
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
test('a debounced save runs once, 300 ms after the last call', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] }); // restored automatically after the test
const save = mock.fn();
let handle;
const debounced = () => { clearTimeout(handle); handle = setTimeout(save, 300); };
debounced(); debounced(); debounced();
t.mock.timers.tick(299);
assert.equal(save.mock.callCount(), 0);
t.mock.timers.tick(1);
assert.equal(save.mock.callCount(), 1);
});
✔ a debounced save runs once, 300 ms after the last call
ℹ tests 1
ℹ pass 1
ℹ fail 0
What changed recently
| Version | Change |
|---|---|
v24.2.0 |
|
v24.0.0 |
The long-deprecated internal helpers |
v23.1.0 |
The test runner’s mock timers ( |
v20.5.0 and v18.18.0 |
|
v18.0.0 |
An invalid callback throws |
v17.3.0 and v16.14.0 |
|
v16.0.0 |
|
v14.9.0 and v12.19.0 |
|
v11.0.0 |
|
The documentation lists no other behavioural change to the timer functions on Node.js 26; the API is stable and slow-moving, and
the differences between the three lines covered here are the ones in the table above plus the NaN and using notes in the
sections before.
Source: Node.js API: Timers and Node.js API: Deprecated APIs
Where the books are outdated
Two O’Reilly books from 2025 touch on timers: Efficient Node.js by Samer Buna and Node.js Projects by Jonathan Wexler. Both are useful for the core idea, and both mostly use the callback forms. What they do not cover, or cover only briefly, is summarised here against the current documentation.
| Topic | Current state |
|---|---|
|
Mentioned briefly in Efficient Node.js (a promise-based |
Polling with |
Node.js Projects polls feeds with |
|
The books use timers as simple delays. They do not use |
|
Newer than both books' examples: stable on timers since Node.js 24.2.0. |
Accuracy and limits |
Neither book discusses the 1 ms minimum, the 2147483647 ms ceiling and its |
Testing |
Efficient Node.js shows |
Removed internals |
Older tutorials may use |
References
-
Node.js API: Timers, https://nodejs.org/docs/latest/api/timers.html
-
Node.js API: Globals (
AbortController,AbortSignal), https://nodejs.org/docs/latest/api/globals.html -
Node.js API: Test runner (Mocking timers), https://nodejs.org/docs/latest/api/test.html
-
Node.js API: Process (
'warning'event), https://nodejs.org/docs/latest/api/process.html -
Node.js API: Performance measurement APIs (
monitorEventLoopDelay), https://nodejs.org/docs/latest/api/perf_hooks.html -
Node.js API: Deprecated APIs (DEP0095, DEP0096, DEP0126, DEP0127), https://nodejs.org/docs/latest/api/deprecations.html
-
Node.js Learn: Discover JavaScript Timers, https://nodejs.org/learn/asynchronous-work/discover-javascript-timers
-
Node.js Learn: The Node.js Event Loop, Timers and process.nextTick(), https://nodejs.org/learn/asynchronous-work/event-loop-timers-and-nexttick
-
Node.js Learn: Understanding setImmediate(), https://nodejs.org/learn/asynchronous-work/understanding-setimmediate
-
WICG: Scheduling APIs draft, https://github.com/WICG/scheduling-apis
-
O’Reilly: Efficient Node.js, Samer Buna, https://www.oreilly.com/library/view/efficient-node-js/9781098145187
-
O’Reilly: Node.js Projects, Jonathan Wexler, https://www.oreilly.com/library/view/node-js-projects/9781098173135/