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 — 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 assertiveassertive 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 — 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 across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

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

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers