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.
- Challengeeasy
Debounce (Simulation)
Implement the timer reset, then prove it fires once — the simulation makes the timing visible.
- Challengeeasy
Throttle (Simulation)
The same shape with a timestamp instead of a timer, which is exactly where the two diverge.
- Quizeasy
Asynchronous Programming · Promises & Callbacks
Timers, callbacks and the scheduling both of these depend on.