Skip to main content
Being Idea Innovations
Auto-Refresh a Div Every 10 Seconds in JavaScript (Without setInterval)
Back to Blog

Auto-Refresh a Div Every 10 Seconds in JavaScript (Without setInterval)

2 September 20185 min readJavaScript
Share:

Refreshing part of a page on a timer (a live count, an order status, a dashboard tile) looks like a two-line job. The two-line version works fine on your machine and then misbehaves in production: requests pile up on slow connections, the tab keeps polling for hours in a background window, and one server error turns into a request every second forever.

Here is how to do it properly with no jQuery and no library.

The quick version

<div id="live" aria-live="polite">Loading…</div>
const target = document.getElementById('live');

async function refresh() {
  try {
    const res = await fetch('/api/status', { headers: { Accept: 'application/json' } });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    const data = await res.json();
    target.textContent = data.message;
  } catch (err) {
    console.error('Refresh failed', err);
  }
}

setInterval(refresh, 10000);
refresh();

That will work. But it has a flaw worth understanding before you ship it.

Why setInterval is the wrong tool

Timeline comparing setInterval firing regardless of request duration against recursive setTimeout waiting for completion setInterval: fires on a fixed clock 0s10s 20s30s request 1 (8s) request 2 (15s, still running) request 3 fires anyway → overlap Recursive setTimeout: waits for completion request 1 request 2: next scheduled only after this finishes request 3
setInterval fires on a fixed schedule regardless of whether the previous request finished. On a slow connection requests overlap and can arrive out of order, so an older response overwrites a newer one.

Three concrete problems:

  • Overlapping requests. If a response takes longer than the interval, a second request starts before the first returns. They can complete out of order, so stale data overwrites fresh data.
  • No backoff. If the server starts returning 500s, setInterval keeps hammering it at exactly the same rate.
  • Drift under throttling. Browsers clamp timers in background tabs, so intervals do not fire when you expect and may fire in a burst on return.

The recursive setTimeout pattern

Schedule the next run only after the current one finishes:

const target = document.getElementById('live');

const BASE_DELAY = 10_000;
const MAX_DELAY  = 5 * 60_000;

let timer = null;
let controller = null;
let failures = 0;
let stopped = false;

async function poll() {
  // Cancel any in-flight request before starting a new one.
  controller?.abort();
  controller = new AbortController();

  try {
    const res = await fetch('/api/status', {
      signal: controller.signal,
      headers: { Accept: 'application/json' },
    });

    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    const data = await res.json();
    render(data);
    failures = 0;                       // success resets the backoff
  } catch (err) {
    if (err.name === 'AbortError') return;   // we cancelled it deliberately
    failures++;
    console.error('Poll failed', err);
  } finally {
    if (!stopped) schedule();
  }
}

function schedule() {
  // Exponential backoff with jitter so many clients do not retry in lockstep.
  const backoff = Math.min(BASE_DELAY * 2 ** failures, MAX_DELAY);
  const jitter  = Math.random() * 1000;
  timer = setTimeout(poll, backoff + jitter);
}

function render(data) {
  target.textContent = data.message;
}

function start() { stopped = false; poll(); }
function stop()  { stopped = true; clearTimeout(timer); controller?.abort(); }

start();

The jitter matters at scale. Without it, if your server briefly goes down, every connected client retries at precisely the same moment and delivers a synchronised traffic spike the instant it recovers. This is the "thundering herd" problem.

Stop polling when nobody is looking

A tab left open in the background will happily poll all night, burning battery and your server's capacity. The Page Visibility API fixes this in a few lines:

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    stop();
  } else {
    failures = 0;   // refresh immediately on return
    start();
  }
});

This is probably the highest-value addition on this page. On a dashboard people leave open in a background tab, it can eliminate the large majority of requests.

Clean up on navigation too, particularly in a single-page app where the component unmounts but your timer does not:

window.addEventListener('pagehide', stop);

Updating only what changed

Replacing a container's contents on every poll destroys and rebuilds DOM nodes, which loses focus, collapses text selection, and resets scroll position. If a user is mid-interaction, that is disruptive.

function render(data) {
  // Only touch the DOM when the value actually changed.
  if (target.dataset.value === String(data.value)) return;

  target.dataset.value = String(data.value);
  target.textContent = data.message;
}

You can also let the server tell you nothing changed, which saves the bandwidth entirely:

let etag = null;

const res = await fetch('/api/status', {
  headers: etag ? { 'If-None-Match': etag } : {},
});

if (res.status === 304) return;    // unchanged, nothing to do
etag = res.headers.get('ETag');

Accessibility

Content that changes on its own is invisible to a screen reader user unless you announce it. Wrap the region in a live region:

<div id="live" aria-live="polite" aria-atomic="true"></div>

Use polite, not assertive. assertive interrupts whatever is being read, which for a routine status update is hostile. And be sparing: a region that updates every ten seconds with an assertive setting makes the page unusable.

When polling is the wrong answer

Polling every 10 seconds means an average 5-second delay and a request per client per interval whether or not anything changed. Past a certain point, push is better:

ApproachDirectionBest for
PollingClient pullsInfrequent changes, simple infrastructure, no persistent connection
Server-Sent EventsServer pushesOne-way live updates: feeds, notifications, progress
WebSocketBoth waysChat, collaborative editing, anything genuinely bidirectional

SSE is dramatically underused. It runs over ordinary HTTP, reconnects automatically, and is about five lines on the client:

const source = new EventSource('/api/stream');

source.addEventListener('message', (e) => render(JSON.parse(e.data)));
source.addEventListener('error', () => {
  // EventSource reconnects on its own: no retry logic needed.
  console.warn('Stream interrupted, reconnecting…');
});

If updates originate on the server and only flow one way, SSE is almost always a better fit than a polling loop.

Summary

  • Use recursive setTimeout, not setInterval, and never let requests overlap.
  • Cancel in-flight requests with AbortController.
  • Back off exponentially on failure, with jitter.
  • Pause when the tab is hidden and clean up on unload.
  • Only touch the DOM when the data actually changed.
  • Announce updates with aria-live="polite".
  • Consider SSE for one-way live data. It is simpler than the polling loop it replaces.
Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea, with 15+ years building scalable software products for global markets and a hands-on role in every architecture decision.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior-led teams
US · AU · NZ timezone coverage
2-week paid trial sprint