Some events fire dozens of times a second — scroll, resize, mousemove, and every keystroke in a search box. If you run expensive work (an API call, a layout recalculation) on each one, you flood the main thread and the network. Debounce and throttle are two rate-limiting techniques that fix this — and they solve different problems.
Debounce: wait until the storm passes
A debounced function delays running until events stop coming for a set period. Every new event resets the timer, so it fires only once, after things go quiet.
Use it when you only care about the final state:
- Search-as-you-type (call the API after the user stops typing)
- Validating a field after the user finishes editing
- Autosaving a draft once edits pause
function debounce(fn, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce((q) => fetch(`/api/search?q=${q}`), 300);
input.addEventListener('input', (e) => search(e.target.value));
Type "laravel" quickly and only one request goes out — 300 ms after the last keystroke.
Throttle: run at a steady rate
A throttled function runs at most once per interval, no matter how many events fire. It doesn't wait for quiet — it guarantees a regular cadence.
Use it when you want periodic updates during a continuous stream:
- Updating a scroll-progress bar
- Recomputing layout on
resize - Rate-limiting rapid clicks
function throttle(fn, interval = 200) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}
const onScroll = throttle(() => updateProgressBar(), 200);
window.addEventListener('scroll', onScroll);
Scroll for 3 seconds and the bar updates ~15 times (every 200 ms), not on all 100+ scroll events.
Which one do you need?
Ask yourself: do you want the result after activity settles, or during it?
- "After it settles" → debounce (search, autosave, validation).
- "At a steady rate while it happens" → throttle (scroll, resize, drag).
In production, reach for a battle-tested implementation like lodash.debounce / lodash.throttle — they handle leading/trailing edges, cancellation, and maxWait for you.

