Some browser events fire far more often than you want to react to them. A search input fires on every keystroke; a scroll or resize handler can run hundreds of times a second; a mousemove floods you with events. Running expensive work on every one of them janks the page. Debounce and throttle are the two standard tools for taming this, and although they sound interchangeable, they behave differently — and choosing the wrong one is a genuine bug, not just an inefficiency.
The one-sentence difference
Here is the whole distinction, and everything else follows from it. Debounce waits until the events stop before running once. Throttle runs at a steady maximum rate while the events continue. Debounce is “do it when they’re done”; throttle is “do it every so often along the way.” That single difference decides which one fits a given situation.
Picture a user typing in a search box. Debounce says: wait until they stop typing for 300 milliseconds, then fire one search. Throttle says: fire a search at most once every 300 milliseconds while they type. For search, debounce is clearly right — you want one query for the finished word, not a query every 300ms mid-word. For a scroll position indicator, throttle is right — you want steady updates as they scroll, not a single update after they stop.
Debounce: wait for the pause
Debounce collapses a burst of events into a single call that happens after the burst ends. Every new event resets the timer, so the function only runs once the events have gone quiet for your chosen interval. If events keep coming, it keeps waiting.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer); // each call cancels the pending one
timer = setTimeout(() => fn(...args), delay);
};
}
// One search fires, delay ms after the user stops typing
searchInput.addEventListener('input', debounce(runSearch, 300));
Debounce is the right choice whenever you only care about the final state after activity settles. Search-as-you-type is the classic case: you want the query for what they finished typing, not for every intermediate string. It also fits auto-saving a form after the user stops editing, validating a field once they pause, or firing a resize handler after the window stops being dragged. The mental test is: “do I only need to act once things have stopped?” If yes, debounce. Debouncing a search box is also a direct interaction-responsiveness win, since it stops a heavy handler from firing on every keystroke.
Throttle: a steady maximum rate
Throttle guarantees the function runs at most once per interval, no matter how many events arrive. Unlike debounce, it does not wait for a pause — it fires regularly during continuous activity, then ignores extra events until the interval has passed.
function throttle(fn, limit) {
let waiting = false;
return (...args) => {
if (waiting) return; // ignore calls during the cooldown
fn(...args);
waiting = true;
setTimeout(() => { waiting = false; }, limit);
};
}
// Runs at most every 100ms while scrolling, giving steady updates
window.addEventListener('scroll', throttle(updateScrollIndicator, 100));
Throttle fits anything where you want continuous-but-limited updates during an ongoing action. A scroll progress bar, a “load more when near the bottom” check, tracking the cursor for a drawing tool, updating a layout during a window resize — all want regular updates as the action happens, not one update after it ends. The test here is: “do I need to keep reacting while it’s happening, just not on every single event?” If yes, throttle.
Choosing between them
Almost every real decision comes down to whether you care about the events during the activity or only about the result after it. Search, auto-save, and validation care about the settled result: debounce. Scroll indicators, infinite-scroll triggers, and drag interactions care about the ongoing activity: throttle. When you catch yourself unsure, ask whether firing only once at the very end would be acceptable — if it would, debounce; if you would miss important intermediate updates, throttle.
Getting it wrong is a visible bug, not a subtle one. Throttle a search box and you fire a query mid-word every 300ms, wasting requests and showing results for half-typed strings. Debounce a scroll indicator and it sits frozen while the user scrolls, then jumps to the right place only after they stop — which feels broken. The behaviors are genuinely different, so the choice matters.
Use a library, and mind the edges
You rarely need to write these yourself. Utility libraries provide battle-tested versions with options the simple implementations above skip — leading versus trailing invocation (should it fire immediately on the first event or only after the interval?), a maximum wait for debounce so it cannot be starved forever by continuous input, and proper cancellation. Those edge cases matter more than they look, and a well-tested implementation handles them so you do not rediscover each one through a bug.
Two practical notes. Preserve this and the event argument if your handler needs them — the minimal versions above forward arguments but a naive rewrite often loses them. And remember these are about rate-limiting how often you react, which is related to but distinct from reducing the cost of each reaction; if the work inside the handler is itself heavy enough to block the main thread, debouncing it less often still leaves you with a slow handler, and that is a different problem — one about the work itself rather than its frequency, and closer to the main-thread cost that governs how a page actually feels.