W3docs

JavaScript Debounce and Throttle

Learn how to rate-limit functions in JavaScript with debounce and throttle — implementation and when to use each.

Some events fire far more often than you can usefully respond to them. Typing into a search box triggers an input event on every keystroke; scrolling a page can emit hundreds of scroll events per second; resize and mousemove are just as chatty. If each event runs expensive work — a network request, a layout calculation, a re-render — your app stutters. Debounce and throttle are two small wrappers that cap how often a function runs, keeping responsiveness high without changing what the function does.

Both are classic decorator patterns: they take a function and return a new function with the same behavior plus a rate-limiting rule. They are built on closures to remember state between calls and on timers like setTimeout to defer or gate execution.

The Core Idea

The two techniques answer the same question — "how often should this run?" — in opposite ways:

  • Debounce waits for a pause. It postpones the call until N milliseconds have passed since the last invocation. If calls keep coming, the timer keeps resetting and the function never fires. Think: "wait for quiet."
  • Throttle enforces a steady cadence. It lets the function run at most once per N milliseconds, no matter how many times it is called in between. Think: "regular heartbeat."
AspectDebounceThrottle
Fires whenActivity stops for N msAt most once every N ms
During a burstNothing runs until the burst endsRuns on a fixed schedule
Mental model"Wait for quiet""Steady cadence"
Good forSearch-as-you-type, autosave, resize-finishedScroll tracking, drag, infinite scroll

Debounce

A debounced function clears any pending timer on each call and schedules a fresh one. Only when the calls finally stop for delay milliseconds does the wrapped function actually run.

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

Two details make this robust. The wrapper collects every argument with rest parameters (...args) and forwards them, so the wrapped function receives exactly what the caller passed. And it invokes fn with fn.apply(this, args) so the original this is preserved — important when the debounced function is a method on an object. (See call and apply and function binding for why forwarding this matters.)

Here it is in action. Calling the wrapped function repeatedly only triggers one real run, after the activity settles:

javascript— editable

Because each keystroke resets the clock, debounce is ideal whenever you want to react after the user finishes: search-as-you-type, autosaving a draft, validating a field once typing stops, or recalculating a layout only when a window resize has settled.

Throttle

A throttled function runs immediately, then ignores further calls until a cooldown elapses. This guarantees a maximum frequency rather than waiting for a pause.

function throttle(fn, limit) {
  let inThrottle = false;
  return function (...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

The inThrottle flag, held in the closure, acts as a gate. The first call passes through and the gate closes; any calls during the cooldown are dropped; when the timer fires, the gate reopens for the next call.

javascript— editable

Throttle fits anything that streams continuously and where you want regular updates rather than every single one: tracking scroll position, handling mousemove during a drag, loading more content on infinite scroll, or capping how often you hit a rate-limited API.

Leading vs. Trailing Edge

There is a subtle design choice in both wrappers: should the function fire on the leading edge (the first call, right away) or the trailing edge (after the delay/cooldown)?

  • The debounce above is trailing: nothing happens until activity stops. A leading-edge debounce would run on the first call and then ignore the rest.
  • The throttle above is leading: it fires immediately and then gates. A trailing-edge throttle would also run once more at the end of the window to capture the final value.

These edge behaviors matter in practice — a trailing throttle on scroll, for example, ensures you don't miss the final scroll position when the user stops.

Info

For production code, prefer a battle-tested implementation such as lodash's _.debounce and _.throttle. They handle leading and trailing edges, a cancel()/flush() API, and a maxWait option (so a debounced function still runs eventually during continuous activity). Understanding the core versions above is essential, but you rarely need to ship your own.

A Real DOM Example

Wiring debounce to a search input is the canonical use case. We attach one listener (see event handling in the DOM) and let the wrapper decide when the work actually runs:

const input = document.querySelector('#search');

function search(event) {
  console.log('Querying API for:', event.target.value);
  // fetch(`/api/search?q=${event.target.value}`) ...
}

const debouncedSearch = debounce(search, 400);

input.addEventListener('input', debouncedSearch);

Now the network request fires only when the user pauses for 400 ms, instead of on every keystroke — a search box that previously fired a dozen requests for hello now fires one. Note that the listener receives the DOM event object, and because our wrapper forwards every argument, search still gets it intact.

Warning

Timers and listeners hold references, so clean them up when they are no longer needed. In a single-page app or component, remove the listener on teardown (for example, on component unmount) and clear any pending timer to avoid memory leaks and callbacks firing on elements that no longer exist:

input.removeEventListener('input', debouncedSearch);

A production debounce typically also exposes a cancel() method that calls clearTimeout for you.

Choosing Between Them

When you are unsure which to reach for, ask what you care about:

  • Do you only care about the final state after a burst of activity (the finished search term, the settled window size)? Use debounce.
  • Do you want continuous, regular feedback during the activity (scroll progress, a dragged element's position)? Use throttle.

Both are lightweight, framework-agnostic, and build directly on closures and timers — the same foundations behind arrow functions capturing this and the scheduling tools you have already seen.

Test Your Knowledge

Practice
Which technique fires the function at most once per fixed time interval, no matter how often it is called?
Which technique fires the function at most once per fixed time interval, no matter how often it is called?
Practice
You want to send a search request only after the user stops typing. Which is the right tool?
You want to send a search request only after the user stops typing. Which is the right tool?
Practice
Why do the example debounce and throttle wrappers call the original function with `fn.apply(this, args)` instead of just `fn(args)`?
Why do the example debounce and throttle wrappers call the original function with `fn.apply(this, args)` instead of just `fn(args)`?
Was this page helpful?