Debounce vs throttle: the difference is who decides when to run
Both limit how often a function runs. Debounce lets the last call decide the moment; throttle uses a fixed window. The wrong pick shows up as a laggy drag or a request every 200ms.
Debounce and throttle get memorized as two similar limiting helpers. What actually separates them is one question: who decides when the function runs.
- Debounce: the last call decides. Every new call resets the timer, and it fires only once things have been quiet long enough.
- Throttle: a fixed window decides. At most one run per window.
Debounce: wait for quiet
function debounce<T extends unknown[]>(fn: (...a: T) => void, wait: number) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: T) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), wait);
};
}
Search-as-you-type uses this: do not send a request while the user is still typing, send when they stop.
Note that it never runs while typing is in progress. Type for ten seconds straight and it fires zero times. That is the point, and it also means a live character count cannot use it.
Throttle: run on a fixed rhythm
function throttle<T extends unknown[]>(fn: (...a: T) => void, interval: number) {
let last = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: T) => {
const now = Date.now();
const remaining = interval - (now - last);
if (remaining <= 0) {
clearTimeout(timer);
last = now;
fn(...args);
} else if (!timer) {
timer = setTimeout(() => {
last = Date.now();
timer = undefined;
fn(...args);
}, remaining);
}
};
}
Scroll progress and drag tracking use this: continuous feedback, without handling every event.
The extra else if (!timer) branch is the important part. With only if (remaining <= 0), the final event is dropped and a drag freezes halfway after release.
Choosing
| Need | Pick | Why |
|---|---|---|
| Search suggestions | debounce | no request while typing |
| Resize relayout | debounce | one relayout after dragging stops |
| Scroll progress bar | throttle | needs continuous feedback |
| Drag tracking | throttle | must follow until release |
| Autosave | debounce | save once editing stops |
Ask one question: can the user perceive each intermediate call? If not, they are safe to drop, so use debounce. If yes, use throttle.
Two common misconceptions
Debounce is always cheaper. Not quite. During continuous input debounce indeed never runs, which is cheaper. But throttle guarantees a steady rate, which is what a mid-gesture state refresh needs.
requestAnimationFrame can replace throttle. rAF only fires while the page is visible, and its rate follows the display (60Hz and 120Hz differ). For purely visual scroll tracking it is the better tool, because it stays in step with painting. But it offers no “at least once every N milliseconds” guarantee.
Debounce manages quiet; throttle manages rhythm. Decide which one you need, and the two functions differ by three lines.

Comments
…