Type "debounce vs throttle" into any search engine and you will get diagrams. Here is the version with numbers attached, which is usually what actually makes it stick.
Debounce: wait for a pause
A search-as-you-type box firing an API call on every keystroke will fire 10+ requests for a 10-character word. Debounce says: wait until the user stops typing for, say, 300ms, then fire once.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
searchInput.addEventListener(
"input",
debounce((e) => fetchResults(e.target.value), 300)
);
Type "react" fast: five keystrokes, zero requests fire until 300ms after the last one — so exactly one request, for "react".
Throttle: enforce a steady maximum rate
A scroll handler updating a "reading progress" bar needs to run repeatedly, just not on every single scroll event (which can fire 60+ times a second). Throttle says: run at most once every N milliseconds, no matter how often the event fires.
function throttle(fn, limit) {
let waiting = false;
return (...args) => {
if (waiting) return;
fn(...args);
waiting = true;
setTimeout(() => (waiting = false), limit);
};
}
window.addEventListener(
"scroll",
throttle(() => updateProgressBar(), 100)
);
Scroll continuously for 2 seconds: instead of 120+ calls, you get roughly 20, evenly spaced — the bar stays smooth without hammering the layout.
The rule of thumb
If the ideal outcome is "run once, after things settle" — search boxes, resize handlers, form validation — debounce. If the ideal outcome is "run regularly, but not too regularly" — scroll position, mouse-move effects, live progress — throttle. Reaching for the wrong one either delays a search that should have felt instant, or floods a rate-limited API that should have been eased off.