Repeated Events: Timeout or Interval?

Every 101 on Javascript will tell you that setTimeout is used for delaying a single action, while setInterval is used for stuff that’s supposed to happen repeatedly.

That’s only half the truth, though: If a function needs to be called repeatedly at a certain interval, you can easily use setTimeout within the delayed function itself to set up a timed loop.

So, there are two ways to do the same thing, one with setInterval:

1
2
3
4
var doStuff = function () {
  // Do stuff
};
setInterval(doStuff, 1000);

The other with setTimeout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// A basic setTimeout loop, mimicking setInterval
var doStuff = function () {
  // Do stuff
   setTimeout(doStuff, 1000);
};
setTimeout(doStuff, 1000);


// If you want the first execution of the function to happen immediately,
// you can change that to
var doStuff = function () {
  // Do stuff
   setTimeout(doStuff, 1000);
};
doStuff();


// ... or, which is the cooler way, use an immediately executed function:
(function doStuff() {
  // Do stuff
   setTimeout(doStuff, 1000);
}());

This inevitably leads to the …

Question: Are setInterval- and self-invoking setTimeout-loops interchangable?

Answer: No, they are not. The difference is somewhat subtle, but if you care about writing good code, this is something you will want to know.

Alright, what’s going to happen is, first, I will tell you what the general issue is, second I’ll start introducing some subtleties (some of which I haven’t seen mentioned anywhere else), which will make one of the two options seem much more attractive than the other, and third I will tell you that you don’t actually need the other one at all. Then there’s the conclusion, which states rather unexcitingly that setTimeout is the superior choice. I will also compliment you on having seen that conclusion coming all along.

Clogging Up The Pipes

Ok, now that all the suspense is gone, let’s get down to business:

First: If the function you are trying to call repeatedly wouldn’t take any time at all to run, there would be no problem. It does, though, and it can do so in two different ways: it can either just do some CPU intensive stuff that takes time, or it can start an action outside of the script’s flow, which will come back later with a result.

Let’s have a look at the second one first. The typical action that runs outside of the script’s flow is an AJAX call: your script won’t sit there and wait for the server to respond, it will just run to the end, and let the callback function worry about the AJAX response.

Now, there are those sites that like to keep you updated on what’s happening on the server, like Gmail refreshing your inbox when you got new mail. There are techniques to make the server actually notify the browser when something like that happens, but more often than not it is done by AJAX polling, meaning that the browser is sending out polls to the server at regular intervals, asking whether there’s anything it should know about.

“Regular intervals, hm?”, you might think, and jump right in there, wielding your good old setInterval:

(Notice that in the following code examples I will be using jQuery syntax to make those AJAX examples a bit more succinct. This doesn’t change anything timing-related.)

1
2
3
4
5
6
7
8
9
10
11
12
// DON'T DO IT LIKE THIS
var pollServerForNewMail = function () {
  $.getJSON('/poll_newmail.php', function (response) {
    if (response.newMail) {
      alert(
        "New mail. At last. You made me walk all the way to the server and back every " +
        "second for this, so if this isn't life-or-death, you got another thing coming."
      );
    }
  });
};
setInterval(pollServerForNewMail, 1000);

That’s no good, though. A trip to the server and back takes time, and who’s to say it takes less than a second? If it takes only slightly more, the line will be constantly clogged up with your polls, and that’s no fun at all.

A typical beginner’s mistake might be to think that making the poll interval a bit longer would be a good way to address this problem. Fact of the matter is, though, that whatever magic number that interval is set to, it still might be too short. What you need is some guaranteed breathing room between two polls, and that’s where setTimeout comes in:

1
2
3
4
5
6
7
8
9
10
11
(function pollServerForNewMail() {
  $.getJSON('/poll_newmail.php', function (response) {
    if (response.newMail) {
      alert(
        "You have received a letter, good sir. " +
        "I will have a quick lie-down and be on my way shortly."
      );
    }
    setTimeout(pollServerForMail, 1000);
  });
}());

The relentlessness of setInterval is completely gone here. Nothing happens until the server response arrives, and when it does, the polling function, that poor bastard, is given a second to relax before having to make the next trip. There’s no chance that more than one polling function is running at any one time, and after every poll there’s a guaranteed pause for other tasks to enjoy the wonders of the internet connection.

Of course, this means that there’s actually more than a second between every two polls, depending on various factors like the connection speed and the time it takes the server to come up with a response. But obviously that’s not a problem for what we’re doing here.

On The Futility of Nagging

So much for actions outside of the script’s flow. The example was an AJAX call, but the general principle applies to all sorts of event handling. The other thing I mentioned are CPU intensive tasks, and that brings me right to the next point, where a few subtleties will start to crop up:

Second: Okay, we’ve seen in that out-of-flow AJAX example how a setTimeout-loop doesn’t act on a strict schedule, but gives even slow tasks a breather, whereas setInterval is a relentless bitch. Let’s have a look at how this works with in-the-flow actions:

The setInterval case:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var timesRun = 0;
var startTime = new Date().getTime();

var doStuff = function () {
  var now = new Date().getTime();

  // Run only 5 times
  if (++timesRun == 5) clearInterval(timer);

  console.log('Action ' + timesRun + ' started ' + (now - startTime) + 'ms after script start');

  // Waste some time
  for (var i = 0; i < 100000; i++) {
    document.getElementById('unobtanium');
  }

  console.log('and took ' + (new Date().getTime() - now) + 'ms to run.');
};

var timer = setInterval(doStuff, 1000);

With the output being something like:

1
2
3
4
5
6
7
8
9
10
Action 1 started 1000ms after script start
and took 71ms to run.
Action 2 started 2001ms after script start
and took 73ms to run.
Action 3 started 3001ms after script start
and took 72ms to run.
Action 4 started 4002ms after script start
and took 112ms to run.
Action 5 started 5003ms after script start
and took 71ms to run.

There are no big surprises here. The actions take some time, but setInterval sticks to its schedule — sort of: you’ll notice that the start times aren’t spaced at exact 1 second intervals. That’s because Javascript’s timing functions just aren’t that reliable. How reliable they actually are, depends on the browser, so if you’re trying to do something time sensitive, you need to synchronize with an actual Date object. We’ll see an example of that later.

Now the setTimeout-loop case:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var timesRun = 0;
var startTime = new Date().getTime();

var doStuff = function () {
  var now = new Date().getTime();

  console.log('Action ' + (timesRun + 1) + ' started ' + (now - startTime) + 'ms after script start');

  // Waste some time
  for (var i = 0; i < 100000; i++) {
    document.getElementById('unobtanium');
  }

  console.log('and took ' + (new Date().getTime() - now) + 'ms to run.');

  // Run only 5 times
  if (++timesRun < 5) {
    setTimeout(doStuff, 1000);
  }
};

setTimeout(doStuff, 1000);

With the output being something like this:

1
2
3
4
5
6
7
8
9
10
Action 1 started 1000ms after script start
and took 73ms to run.
Action 2 started 2075ms after script start
and took 78ms to run.
Action 3 started 3154ms after script start
and took 74ms to run.
Action 4 started 4229ms after script start
and took 81ms to run.
Action 5 started 4312ms after script start
and took 81ms to run.

No surprises here either; we already know that a setTimeout-loop won’t stick to the schedule, and instead give its function all the time it needs before calling it again.

Okay, all of that looked very familiar, so what exactly is the difference between this and the out-of-flow case? Consider this:

In the example in the last section I lead you to imagine the timer callback as this poor guy who gets a good whipping if he doesn’t trudge to the server to check for new mail every second, but you may have noticed that if the AJAX call hasn’t returned after a second, setInterval won’t care a bit — in fact, it doesn’t even know — and just send another one out (there can be lots of AJAX requests going on at the same time). So, it’s more like there’s a whole army of those guys, and setInterval sends one of them off into battle every second without even thinking about it, whereas the setTimeout-loop will wait for each one of them to return and be properly debriefed before sending out the next.

Now, in the case of in-the-flow actions, there’s really only one guy — unlike requests to the server and the like, no two pieces of Javascript can run at the same time. The setTimeout-loop has no reason to do anything differently here, but setInterval is in a bit of a pickle: it hates falling behind schedule, but if it’s time to start up the next action, when the previous one hasn’t even finished yet, no amount of nagging will change anything about that. setInterval has a choice to make: if the next call is already late, it can either 1) start it immediately, the first chance it gets, or 2) wait for the next scheduled execution time (in our case the next full second after script start).

Okay, what’s actually happening? The answer is 1), so if you thought you could use setInterval to set up a metronome that might occasionally miss a beat, but will never click out of rhythm, you thought wrong.

The World’s Worst Drummer

After it did miss a beat and awkwardly slipped in the next one out of rhythm, it has another choice to make: a) it can pretend this never happened and just get back into the original rhythm, or b) it can say “fuck it” and start a whole new rhythm, based on that last wrong beat.

Why don’t you see for yourself how it chooses ( — it depends on your system speed how much time it will actually waste, so you can adjust those variables in the beginning to make sure it actually does miss a beat):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var timeWastingCycles = 5000000;
var timeBetweenCalls = 1000; // Milliseconds
var numberOfCalls = 5;
             
var timesRun = 0;
var startTime = new Date().getTime();

var wasteSomeTime = function (cycles) {
  var startTime = new Date().getTime();
  for (var i = 0; i < cycles; i++) {
    document.getElementById('unobtanium');
  }
  console.log('Wasting ' + (new Date().getTime() - startTime) + 'ms');
};

var timer = setInterval(function () {
  var now = new Date().getTime();

  console.log('Beat at ' + (now - startTime) + 'ms');
  timesRun++;

  // After the first beat, waste enough time for the timer to miss a beat
  if (timesRun == 1) {
    wasteSomeTime(timeWastingCycles);
  }

  if (timesRun >= numberOfCalls) {
    clearInterval(timer);
  }

}, timeBetweenCalls);

What were your findings? The truth is that the result is browser dependent: Firefox is of the a) type and does its best to get back on schedule, as long as it has only missed one beat. If a long action forces Firefox to miss more than that one single beat, though, it will change to b) and won’t even try to catch up any more. All the other browsers are of the “fuck it” b) persuasion from the get-go, regardless of how many beats they miss. And the fact that you don’t know which browser your code is running in (and you certainly don’t want to resort to browser sniffing, especially since this is totally undocumented behavior) means that it would be really silly to use setInterval with CPU-intensive tasks (or with short intervals) for anything remotely time sensitive.

Layoff Time

So, the thing is flawed. What’s the solution, though? Kick it to the curb! Here’s how to make do without:

Third: I mentioned a bunch of different behaviors before, and it’s interesting to note that it’s very easy to rebuild each and every one of those with a simple setTimeout and a Date object.

Behavior 1a) (after missing a beat, do the next one immediately, and then fall back into the rhythm), which is implemented at least partially by Firefox’s setInterval, can be done like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var timeBetweenCalls = 1000; // Milliseconds
var numberOfCalls = 5;
var timeWastingCycles = 5000000;

var timesRun = 0;
var startTime = new Date().getTime();

var wasteSomeTime = function (cycles) {
  var startTime = new Date().getTime();
  for (var i = 0; i < cycles; i++) {
    document.getElementById('unobtanium');
  }
  console.log('Wasting ' + (new Date().getTime() - startTime) + 'ms');
};

(function timer () {
  var now = new Date().getTime();
  timesRun++;

  // This is the interesting part!
  // Every time the timer is set, the interval is recalculated, based on the start time
  if (timesRun < numberOfCalls) {
    setTimeout(timer, timeBetweenCalls - ((now - startTime) % timeBetweenCalls));
  }

  console.log('Beat at ' + (now - startTime) + 'ms');

  // After the first beat, waste enough time for the timer to miss a beat
  if (timesRun == 1) {
    wasteSomeTime(timeWastingCycles);
  }

  // After the third beat, waste a little time
  if (timesRun == 3) {
    wasteSomeTime(50000);
  }
})();

Since it resynchronizes with the start time on every run-through, it can run for a long time without Javascript’s timing functions’ inherent inaccuracies accumulating.

Behavior 1b) (after missing a beat, do the next one immediately and start a new rhythm from there), which all of the browsers’ setIntervals at least partially implement, can be done like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var timeBetweenCalls = 1000; // Milliseconds
var numberOfCalls = 5;
var timeWastingCycles = 5000000;

var timesRun = 0;
var startTime = new Date().getTime();

var wasteSomeTime = function (cycles) {
  var startTime = new Date().getTime();
  for (var i = 0; i < cycles; i++) {
    document.getElementById('unobtanium');
  }
  console.log('Wasting ' + (new Date().getTime() - startTime) + 'ms');
};

(function timer () {
  var now = new Date().getTime();
  var timeout;
  timesRun++;

  // This is the interesting part!
  // Schedule the next invocation a second from now, in case everything goes right
  if (timesRun < numberOfCalls) {
    timeout = setTimeout(timer, timeBetweenCalls);
  }

  console.log('Beat at ' + (now - startTime) + 'ms');

  // After the first beat, waste enough time for the timer to miss a beat
  if (timesRun == 1) {
    wasteSomeTime(timeWastingCycles);
  }

  // After the third beat, waste a little time
  if (timesRun == 3) {
    wasteSomeTime(50000);
  }

  // This is the interesting part!
  // In case a beat was missed, cancel the timeout, and just go from there, setting a new rhythm
  if (timesRun < numberOfCalls) {
    if (new Date().getTime() - now > timeBetweenCalls) {
      clearTimeout(timeout);
      setTimeout(timer);
    }
  }
})();

This doesn’t resynchronize when setting the timeout, so it will accumulate an error, just like setInterval would. This can be easily remedied, though, by making some small changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var timeBetweenCalls = 1000; // Milliseconds
var numberOfCalls = 5;
var timeWastingCycles = 5000000;

var timesRun = 0;
var startTime = new Date().getTime();
// Since the rhythm can change, remember its start time
var startRhythm = startTime;

var wasteSomeTime = function (cycles) {
  var startTime = new Date().getTime();
  for (var i = 0; i < cycles; i++) {
    document.getElementById('unobtanium');
  }
  console.log('Wasting ' + (new Date().getTime() - startTime) + 'ms');
};

(function timer () {
  var now = new Date().getTime();
  var afterExecutionTime, timeout;
  timesRun++;

  // This is the interesting part!
  // Just like before, only now synchronize with the start of the current rhythm
  if (timesRun < numberOfCalls) {
    timeout = setTimeout(timer, timeBetweenCalls - (new Date().getTime() - startRhythm) % timeBetweenCalls);
  }

  console.log('Beat at ' + (now - startTime) + 'ms');

  // After the first beat, waste enough time for the timer to miss a beat
  if (timesRun == 1) {
    wasteSomeTime(timeWastingCycles);
  }

  // After the third beat, waste a little time
  if (timesRun == 3) {
    wasteSomeTime(50000);
  }

  // This is the interesting part!
  // Just like before, only now remember when exactly a new rhythm started,
  // so future invocations can be based on this
  if (timesRun < numberOfCalls) {
    afterExecutionTime = new Date().getTime();
    if (afterExecutionTime - now > timeBetweenCalls) {
      startRhythm = afterExecutionTime;
      clearTimeout(timeout);
      setTimeout(timer, timeBetweenCalls);
    }
  }
})();

Behavior 2) (after missing a beat, just wait for the next one in order to keep the rhythm, no matter what), isn’t implemented by any browser’s setInterval, but can be done like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var timeBetweenCalls = 1000; // Milliseconds
var numberOfCalls = 5;
var timeWastingCycles = 5000000;

var timesRun = 0;
var startTime = new Date().getTime();

var wasteSomeTime = function (cycles) {
  var startTime = new Date().getTime();
  for (var i = 0; i < cycles; i++) {
    document.getElementById('unobtanium');
  }
  console.log('Wasting ' + (new Date().getTime() - startTime) + 'ms');
};

(function timer () {
  var now = new Date().getTime();
  timesRun++;

  console.log('Beat at ' + (now - startTime) + 'ms');

  // After the first beat, waste enough time for the timer to miss a beat
  if (timesRun == 1) {
    wasteSomeTime(timeWastingCycles);
  }

  // After the third beat, waste a little time
  if (timesRun == 3) {
    wasteSomeTime(50000);
  }

  // This is the interesting part!
  // Synchronize with the start time every run
  if (timesRun < numberOfCalls) {
    setTimeout(timer, timeBetweenCalls - ((new Date().getTime() - startTime) % timeBetweenCalls));
  }
})();

The Gist

So, what’s the conclusion? You probably saw it coming all along: don’t use setInterval, if you care anything about your timing. Even without the rather erratic behavior it presents when being forced to miss a beat or two, it’s just not very exact. It’s marginally easier to set up than a recursive setTimeout-loop, but the latter gives you all the control you need to give your scripts and callbacks enough time to breathe, and tops it off with actually correct long-term timing, if you throw a Date object into the mix.