Skip to the content.

Debounce and throttle are different tools for different problems

Debounce and throttle are different tools for different problems

Debounce and throttle both reduce how often a function runs in response to a burst of events, and they are constantly confused because that one-line summary makes them sound interchangeable. They are not. They answer different questions. Debounce asks “has the user stopped?” and runs once after the burst goes quiet. Throttle asks “has enough time passed?” and runs at a steady maximum rate during the burst. Pick the wrong one and the feature feels broken in a way that is hard to name — a search box that fires on every keystroke, or a scroll handler that stutters.

Debounce fires once after quiet; throttle fires at a steady rate A row of rapid event ticks. Debounce produces one call after the events stop. Throttle produces evenly spaced calls during the burst. A moving dot rides the debounce timeline and fires at the end. events debounce one call, after quiet throttle steady calls, every N ms during the burst
Same burst of events. Debounce waits for the gap and fires once; throttle ignores the gaps and fires on a fixed cadence.

Debounce: wait for the user to stop

Debounce delays the call until the events pause for a set interval. Every new event resets the timer, so nothing runs until the burst goes quiet. This is exactly right for a search-as-you-type box: you want to hit the API once the user finishes typing, not on every keystroke.

function debounce(fn, wait) {
  let timer;
  return (...args) => {
    clearTimeout(timer);                 // each event cancels the pending call
    timer = setTimeout(() => fn(...args), wait);
  };
}

const search = debounce((q) => fetchResults(q), 300);
input.addEventListener("input", (e) => search(e.target.value));
// types "cats" fast → ONE fetch, 300ms after the last keystroke

Use debounce for search inputs, autosave-after-editing, resize handlers that recompute a layout, and validating a field once the user leaves it. The tell is “do the work once, when the activity settles.”

Throttle: enforce a steady maximum rate

Throttle runs the function at most once per interval during a continuous burst — it does not wait for quiet, it caps the rate. This is what you want for a scroll or mousemove handler: you need periodic updates while the burst is still happening, just not sixty of them a second.

function throttle(fn, interval) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= interval) {        // enough time passed? run and mark
      last = now;
      fn(...args);
    }
  };
}

const onScroll = throttle(() => updateStickyHeader(), 100);
window.addEventListener("scroll", onScroll);
// scrolls continuously → the header updates ~10x/sec, smoothly, not per frame

Picking the wrong one is the bug

Debounce a scroll handler and the sticky header never moves while you scroll — it only snaps into place after you stop, because debounce is still waiting for quiet that a continuous scroll never gives it. Throttle a search box and you fire a request every 300ms during typing, hammering the API with queries for half-typed words. The question to ask is never “which is faster” — it is “do I want the result after the activity, or during it?” After the burst, debounce; throughout the burst, throttle. The debounce-utility exercise builds the timer-reset version above and is the fastest way to make the distinction stick in your hands rather than just your notes.