timersPromises.setInterval()

Returns an async iterator that yields a value every delay milliseconds until you break out.

Signature
timersPromises.setInterval([delay[, value[, options]]])
  • Added in Node.js v15.9.0

Parameters

ParameterTypeDescription
$delayoptional<number>= 1The number of milliseconds to wait between iterations.
$valueoptional<any>A value with which the iterator returns.
$optionsoptional<Object>Options controlling the Timeout scheduled between iterations.
$options.refoptional<boolean>= trueSet to false to indicate that the scheduled Timeout between iterations should not require the Node.js event loop to remain active.
$options.signaloptional<AbortSignal>An optional AbortSignal that can be used to cancel the scheduled Timeout between operations.

Return value

<AsyncIterator>An async iterator that generates value at an interval of delay ms.

Errors

The iterator rejects with an AbortError (code ABORT_ERR) if options.signal is aborted while waiting.

Changelog

  • PHP v16.0.0The node:timers/promises API graduated from experimental to stable.

From node:timers/promises, this is the async-iterator form of a repeating timer. Instead of a callback you get a for await loop, so the body can await freely and the loop's own control flow — break, return, throw — replaces clearInterval().

When to reach for it

  • Polling that involves awaited work on each tick.
  • Pacing a sequence of API calls without nesting callbacks.
  • Any loop that reads more naturally as code than as a callback plus a flag.

What trips people up

The loop bodies never run concurrently with each other — for await is sequential — but the timer underneath does not wait for them. Node keeps a real interval ticking and counts the ticks you have not consumed yet, so a body that outlasts delay makes the iterator yield again immediately, several times back-to-back if it fell several ticks behind. The average cadence therefore stays at delay rather than becoming delay plus the body's duration; it just arrives in bursts after a slow iteration. If you need a guaranteed gap after each body, await a timersPromises.setTimeout() at the end of the body instead of relying on the iterator.

There is no handle to clear. Exiting is entirely on you: break out, or abort a signal passed in options. An abort while the iterator is waiting rejects, so a signal-driven loop needs a try/catch around it. Forget both and the loop runs forever, and being ref'd by default it keeps the process alive with it.

Note that value is a single fixed value yielded on every iteration, not a per-tick computation — the docs' example passes Date.now() as a captured start time, not a live clock.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Poll a long-running job until it completes or a deadline passes
  • Send batches to an API at a fixed rate without exceeding a quota
  • Stream periodic health snapshots until shutdown is signalled
  • Retry a connection on a fixed cadence inside async code

Frequently asked questions

How do I stop a timers/promises setInterval loop?

Break out of the for-await loop, or abort an AbortSignal passed in the options — there is no handle and no clearInterval equivalent. An abort that lands while the iterator is waiting rejects with an AbortError, so wrap the loop in try/catch.

Does the async iterator version prevent overlapping ticks?

The loop bodies themselves never overlap — for await runs them one at a time — but the timer underneath keeps ticking while a body is awaited. Node counts the ticks you did not consume, so a body that runs longer than delay is followed by iterations that yield immediately, with no wait, until the backlog is cleared; the average period stays at delay rather than becoming delay plus the body's duration. To guarantee a gap after each body, await timersPromises.setTimeout(delay) at the end of it.

What does the value argument actually yield?

The same fixed value on every iteration — it is captured once when the iterator is created, not recomputed per tick. Passing Date.now() gives you a constant start timestamp, which is useful for measuring elapsed time inside the loop.

More examples

Polling a job with a deadline and an abort signal

Each line shows elapsed milliseconds against the job status, because the yielded value is the start timestamp captured once when the iterator was created. The AbortError branch is what turns a cancelled wait into a meaningful timeout error instead of an opaque rejection.

Example · javascript
import { setInterval as every } from 'node:timers/promises';

async function waitForJob(id, { pollMs = 1000, timeoutMs = 30_000 } = {}) {
  const ac = new AbortController();
  const stop = setTimeout(() => ac.abort(), timeoutMs);

  try {
    for await (const startedAt of every(pollMs, Date.now(), { signal: ac.signal })) {
      const job = await fetchJob(id);
      console.log(`${Date.now() - startedAt}ms elapsed -> ${job.status}`);

      if (job.status === 'succeeded') return job;
      if (job.status === 'failed') throw new Error(`job ${id} failed`);
    }
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error(`job ${id} did not finish within ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    clearTimeout(stop);
  }
}

Rate-limiting a batch of requests

The requests are issued one at a time, since the loop body is awaited before the next iteration begins. The iterator's timer keeps running during that await, though, and Node counts the ticks that went unconsumed — so a request slower than 200ms is followed by iterations that yield straight away to catch up, not by another 200ms wait. The result is an average of five requests per second with bursts after a slow call, rather than a guaranteed 200ms gap; if the quota needs a hard floor, await a timersPromises.setTimeout(200) after each request instead.

Example · javascript
import { setInterval as every } from 'node:timers/promises';

const urls = [
  '/users/1', '/users/2', '/users/3', '/users/4', '/users/5',
];

const ticker = every(200);          // at most 5 requests/second
let i = 0;

for await (const _ of ticker) {
  const path = urls[i++];
  const res = await fetch(`https://api.example.com${path}`);
  console.log(path, res.status);

  if (i === urls.length) break;
}
console.log('all', urls.length, 'requests done');

A shutdown-aware background loop

Two things keep this loop from outliving the app: ref: false stops it being the reason Node stays up, and the SIGTERM-driven abort ends it deterministically. Swallowing only AbortError means a genuine failure inside the loop still propagates.

Example · javascript
import { setInterval as every } from 'node:timers/promises';

const ac = new AbortController();
process.on('SIGTERM', () => ac.abort());

async function reportHealth() {
  try {
    for await (const _ of every(5000, null, { signal: ac.signal, ref: false })) {
      const { heapUsed } = process.memoryUsage();
      console.log('heap:', Math.round(heapUsed / 1024 / 1024), 'MB');
    }
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
    console.log('health reporting stopped');
  }
}

reportHealth();

Discussion

  • Be the first to comment on this lesson.