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
Nmilliseconds 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
Nmilliseconds, no matter how many times it is called in between. Think: "regular heartbeat."
| Aspect | Debounce | Throttle |
|---|---|---|
| Fires when | Activity stops for N ms | At most once every N ms |
| During a burst | Nothing runs until the burst ends | Runs on a fixed schedule |
| Mental model | "Wait for quiet" | "Steady cadence" |
| Good for | Search-as-you-type, autosave, resize-finished | Scroll 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:
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.
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
debounceabove is trailing: nothing happens until activity stops. A leading-edge debounce would run on the first call and then ignore the rest. - The
throttleabove 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.
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.
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.