setInterval()
Repeatedly calls a function with a fixed delay between each invocation.
setInterval(callback[, delay[, ...args]])- Added in Node.js v0.0.1
Parameters
| Parameter | Type | Description |
|---|---|---|
$callback | <Function> | The function to call when the timer elapses. |
$delayoptional | <number>= 1 | The number of milliseconds to wait before calling the callback. |
$...argsoptional | <any> | Optional arguments to pass when the callback is called. |
Return value
<Timeout>A Timeout object that can be passed to clearInterval() to stop the repetition.
Errors
Throws ERR_INVALID_ARG_TYPE if callback is not a function.
Changelog
- PHP v18.0.0Passing an invalid callback to the callback argument now throws ERR_INVALID_ARG_TYPE instead of ERR_INVALID_CALLBACK.
setInterval() schedules callback to run every delay milliseconds until you stop it. It returns the same Timeout class setTimeout() returns, so ref(), unref(), refresh() and hasRef() all work on it.
When to reach for it
- Heartbeats, metrics flushes, cache sweeps — cheap synchronous work on a fixed cadence.
- Polling an external state you cannot subscribe to.
What trips people up
The interval does not wait for your callback. If the work is asynchronous and takes longer than delay, invocations pile up and run concurrently — two overlapping database polls, duplicate writes, growing memory. Guard with a running flag, or chain setTimeout() calls after each completion instead.
The delay is also a minimum between scheduling points, so intervals drift. Do not use one as a clock; compute elapsed time from Date.now() if it matters.
The biggest operational surprise is process lifetime: an interval is ref'd, so the process will never exit on its own while one is active. A forgotten interval in a CLI or a test is the classic "my script hangs at the end" bug. Every setInterval() needs a matching clearInterval(), or an unref() if it is purely background.
Delays outside 1–2147483647 are clamped to 1, giving an accidental busy loop.
Example
When to use it
- Flush buffered metrics to disk every 10 seconds
- Poll a job's status until it reports completion
- Send a keep-alive ping on an idle connection
- Sweep expired entries out of an in-memory cache
Frequently asked questions
Why won't my Node script exit when it uses setInterval?
Because an active interval keeps the event loop alive indefinitely — Node only exits when nothing is left to do. Call clearInterval() with the returned handle when you are finished, or call unref() on it if the timer is only background work.
Does setInterval wait for an async callback to finish?
No. It fires every delay milliseconds regardless, so an async callback that takes longer than the interval will overlap with the next run. Use a boolean in-flight guard, or chain setTimeout() after each completion, to serialize the work.
Is setInterval accurate enough to use as a clock?
No — the delay is a minimum between scheduling points, so the timer drifts as event-loop work delays each fire. Track elapsed time with Date.now() and correct the next delay yourself if accuracy matters.
More examples
Stopping overlapping async runs
The 'skipping this tick' line is what you would see whenever checkJob() outruns the 500ms interval. Without the inFlight guard those calls would run concurrently, because setInterval never waits for an async callback to settle.
let inFlight = false;
let attempts = 0;
const poll = setInterval(async () => {
if (inFlight) {
console.log('previous poll still running, skipping this tick');
return;
}
inFlight = true;
try {
attempts += 1;
const status = await checkJob(); // may take longer than 500ms
console.log('attempt', attempts, '->', status);
if (status === 'done' || attempts >= 10) {
clearInterval(poll);
console.log('polling stopped');
}
} catch (err) {
clearInterval(poll);
console.error('polling aborted:', err.message);
} finally {
inFlight = false;
}
}, 500);Avoiding drift with a self-scheduling timeout
Each tick reports how far it landed from its intended slot and corrects the next delay. A plain setInterval accumulates that drift instead, so after an hour it can be seconds behind wall-clock — this pattern keeps it bounded.
const PERIOD = 1000;
const start = Date.now();
let expected = start + PERIOD;
function tick() {
const drift = Date.now() - expected;
console.log('drift this tick:', drift, 'ms');
expected += PERIOD;
// Subtract the drift so the next fire lands back on the grid.
setTimeout(tick, Math.max(0, PERIOD - drift));
}
setTimeout(tick, PERIOD);A background interval that does not block shutdown
Shows the two things a long-lived interval inside a library needs: unref() so the consumer's process can still exit, and an explicit close() that clears the timer and flushes what is left. Without close(), buffered metrics are lost on shutdown.
class MetricsClient {
constructor(flushMs = 10_000) {
this.buffer = [];
this.timer = setInterval(() => this.flush(), flushMs);
// Library-owned timers should never hold the host process open.
this.timer.unref();
}
record(name, value) {
this.buffer.push({ name, value });
}
flush() {
if (this.buffer.length === 0) return;
console.log('flushing', this.buffer.length, 'metrics');
this.buffer.length = 0;
}
close() {
clearInterval(this.timer);
this.flush();
}
}
const metrics = new MetricsClient();
metrics.record('requests', 1);
process.on('SIGTERM', () => metrics.close());
Discussion