Listening to
JavaScript
Performance
Web
Building a Better `setInterval`
It's time to take a second look at how you schedule recurring tasks in JavaScript.
TRACTION
iamtraction
3 min readJan 5, 2020

Timers are deceptively simple in JavaScript. Call setInterval(fn, delay) and boom, your function runs repeatedly. But under the hood, setInterval has sharp edges like overlapping executions, piling up calls, and imprecise control that many developers don't see until they hit real-world timing issues.

When I say setInterval causes "overlapping executions", I mean it schedules new runs even if the previous one hasn't finished. In JavaScript, these executions are actually queued and never run at the exact same time. Still, this can lead to multiple pending executions piling up, which can cause performance issues.

In this post, we'll walk through why setInterval can be problematic, and how to implement a smarter, safer version of it yourself. You'll end up with a reusable utility that behaves like setInterval, but without its flaws.

The Problem with setInterval

Let's start with what happens if you naively use setInterval for a task that takes longer than its interval:

// simulate a task that takes 3 seconds
function sleep(ms) {
    const start = Date.now();
    while (Date.now() - start < ms) {
        // busy wait
    }
}

let count = 0;
function tick() {
    const now = new Date().toLocaleTimeString();
    console.log(`[${ now }] Tick ${ ++count } START`);
    sleep(3000);
    console.log(`[${ new Date().toLocaleTimeString() }] Tick ${ count } END`);
}

// execute tick() every 1 second
setInterval(tick, 1000);

You might expect this to run every second. Instead, you get overlapping calls:

[19:50:01] Tick 1 START
[19:50:04] Tick 1 END
[19:50:04] Tick 2 START
[19:50:07] Tick 2 END
[19:50:07] Tick 3 START
[19:50:10] Tick 3 END
...

Each execution lasts 3 seconds, but since setInterval doesn't wait for one execution to finish before scheduling the next, the timer keeps firing, causing call stacking and potentially freezing your app.

The Safer Alternative

Here's the idea: recursive setTimeout. Instead of setting a fixed interval that blindly schedules calls, we wait until the current execution completes before scheduling the next one.

function betterInterval(task, delay) {
    const wrapper = async () => {
        await task();
        setTimeout(wrapper, delay);
    };
    wrapper();
}

This version avoids overlapping calls and guarantees a minimum delay between the end of one run and the start of the next.

[20:10:01] Tick 1 START
[20:10:04] Tick 1 END
[20:10:05] Tick 2 START
[20:10:08] Tick 2 END
[20:10:09] Tick 3 START
[20:10:12] Tick 3 END
...

When this Is better

Use CaseBetterCustom Interval?
Lightweight periodic loggingsetInterval is fine
Network pollingPrevents overlapping requests
CPU-heavy tasksAvoids browser lock-ups
Real-time UI animationUse requestAnimationFrame
Dynamically adjustable loopsUse setTimeout based loop

Let's take it further. Here's a custom utility that:

  • Prevents overlaps
  • Maintains accurate timing (like setInterval)
  • Supports start, stop, and dynamic delay updates
function createInterval(task, delay) {
    let timerId = null;
    let running = false;

    const loop = async () => {
        const start = performance.now();
        await task();

        const elapsed = performance.now() - start;
        const nextDelay = Math.max(0, delay - elapsed);

        if (running) {
            timerId = setTimeout(loop, nextDelay);
        }
    };

    return {
        start() {
            if (!running) {
                running = true;
                loop();
            }
        },
        stop() {
            running = false;
            if (timerId) {
                clearTimeout(timerId);
            }
        },
        setDelay(newDelay) {
            delay = newDelay;
        },
    };
}

// example usage
const smartInterval = createInterval(tick, 1000);
smartInterval.start();
smartInterval.stop();
smartInterval.setDelay(500);

Final Thoughts

setInterval is easy, but it's often too blunt an instrument for real-world timing needs. If your task is asynchronous, long-running, or dynamically timed, it's worth reaching for a smarter solution.

By building your own interval logic using setTimeout, you avoid overlapping calls, gain precise control, ensure better performance, and it gives you the control you didn't know you were missing.

...
Logo
© 2025 - TRACTION