codejump
Academy

Debounce vs throttle

Two ways to fire less often, and the one question that tells you which you need — do you want the last call, or a steady rate?

Updated

The distinction in one line

Debounce waits for quiet. Throttle enforces a rate.

Given a burst of calls, a debounced function runs once, at the end. A throttled function runs at regular intervals throughout. Everything else is detail.

calls      │ ●●●●●●●●●●●●●●        ●●●●
debounced  │              ▲             ▲     (once, after the burst)
throttled  │ ▲   ▲   ▲   ▲          ▲   ▲     (at most once per interval)

Debounce — "tell me when they stop"

Every call resets the timer. The work only happens once nothing has happened for a while.

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

That timer variable is the whole trick, and it lives in a closure — one per debounced function, invisible to callers.

Reach for it when only the final state matters:

  • a search box that queries as you type
  • validating a field after the user pauses
  • saving a draft
  • reacting to a window resize, which fires dozens of times per drag

Throttle — "no more than this often"

The first call goes through, then further calls are ignored until the window expires.

function throttle(fn, interval) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn(...args);
    }
  };
}

Reach for it when the stream itself is the signal and you want samples of it:

  • scroll position (infinite scroll, a reading progress bar)
  • mouse move during a drag
  • a rate-limited API you must not exceed
  • analytics events on a firehose

The mistake that looks like it works

Debouncing a scroll handler is the classic one. It is not wrong in the sense of throwing — it is wrong in the sense of doing nothing useful:

window.addEventListener('scroll', debounce(updateProgressBar, 200));

The bar now updates only after the user *stops* scrolling. During the scroll — the entire time the bar is being looked at — it is frozen. Throttle is what that handler wanted.

The reverse mistake is throttling a search input: you fire a query every 300 ms while someone types "javascript", sending six requests where one was wanted, and the last one to arrive may not be the last one sent.

Details a real implementation needs

The five-line versions above are the idea, not the library. Production versions carry three more things, and they are what interview questions probe:

Leading and trailing. Should the first call fire immediately, the last one, or both? A debounce with leading: true responds instantly then goes quiet — often what a button wants.

Cancellation. A React component that unmounts with a pending debounced call will still fire it, against a component that no longer exists. Returning a .cancel() and calling it on cleanup is not optional in a UI.

`this` and the return value. An arrow function drops the caller's this; a real implementation forwards it. And a debounced function cannot meaningfully return anything — the call that produced the value happened after the caller moved on. Libraries return the *previous* result, which surprises people; a promise-returning version is usually clearer.

Neither is a rate limiter for a server. Both are per-instance and in-memory: refresh the page and the window is gone. Server-side limits belong on the server.

Now practice it

Reading this page is the cheap half. These are the exercises that make you use it.