Debouncing vs throttling — what's the difference?
The Short Answer
Both debouncing and throttling limit how often a function executes, but they do it differently. Debouncing waits until the user stops doing something, then fires once. Throttling fires at a steady interval no matter how many times the event occurs. Think of debounce as "wait until they're done" and throttle as "at most once every X milliseconds."
Debouncing Explained
Debouncing delays execution until a burst of events stops. Every time the event fires, the timer resets. The function only runs once the user has been idle for the specified delay. This is perfect for actions where you only care about the final value — like a search input where you want to wait until the user finishes typing before making an API call.
The Elevator Analogy
Debouncing is like an elevator door. Every time someone approaches, the door stays open (timer resets). It only closes once nobody has approached for a few seconds. You don't want the door closing and reopening for every single person — you wait until everyone's in.
The implementation below shows how debounce works internally. Each call clears the previous timer and sets a new one. The function only executes when no new calls arrive within the delay window. Notice how clearTimeout is the key mechanism — it cancels the pending execution every time a new event arrives.
function debounce<T extends (...args: any[]) => void>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timeoutId); // Reset timer on every call
timeoutId = setTimeout(() => fn(...args), delay);
};
}
// Usage: search only fires 300ms after user stops typing
const handleSearch = debounce((query: string) => {
fetch(`/api/search?q=${query}`);
}, 300);
input.addEventListener('input', (e) => {
handleSearch(e.target.value);
});
If the user types "react hooks" quickly, the API call only fires once — 300ms after the last keystroke. Without debouncing, you'd fire a request for "r", "re", "rea", "reac", "react", "react ", "react h"... that's 11 unnecessary network requests.
Throttling Explained
Throttling guarantees a function runs at most once per time interval, regardless of how many times the event fires. Unlike debounce, it doesn't wait for silence — it fires immediately on the first call, then ignores subsequent calls until the interval passes. This gives you regular, predictable execution during continuous events.
The Faucet Analogy
Throttling is like a faucet with a flow limiter. No matter how hard you turn the handle, water only comes out at a fixed rate. Events keep firing, but the function only executes at a steady pace — once every X milliseconds.
The throttle implementation below uses a boolean flag to track whether the function is in its cooldown period. The first call executes immediately and sets the flag. Subsequent calls during the cooldown are ignored. Once the interval passes, the flag resets and the next call goes through.
function throttle<T extends (...args: any[]) => void>(
fn: T,
interval: number
): (...args: Parameters<T>) => void {
let isThrottled = false;
return (...args: Parameters<T>) => {
if (isThrottled) return; // Ignore calls during cooldown
fn(...args); // Execute immediately
isThrottled = true;
setTimeout(() => {
isThrottled = false; // Allow next call after interval
}, interval);
};
}
// Usage: update position at most every 100ms during scroll
const handleScroll = throttle(() => {
updateScrollPosition(window.scrollY);
}, 100);
window.addEventListener('scroll', handleScroll);
During a scroll event that fires 60+ times per second, throttle ensures your handler only runs every 100ms — roughly 10 times per second. That's enough for smooth UI updates without overwhelming the browser.
Side-by-Side Comparison
| Aspect | Debounce | Throttle |
|---|---|---|
| When it fires | After the events stop (idle period) | At regular intervals during events |
| First call | Delayed until silence | Executes immediately |
| During rapid events | Keeps resetting, never fires | Fires once per interval |
| Guarantees execution? | Only after events stop | Yes — at a steady rate |
| Best for | Final value matters (search, resize end) | Continuous feedback (scroll, drag, resize progress) |
When to Use Which
Use debounce when:
Debounce use cases
- ✅Search input — wait until user finishes typing before querying
- ✅Window resize — recalculate layout only after resizing stops
- ✅Form validation — validate after user stops changing a field
- ✅Auto-save — save draft after user stops editing
Use throttle when:
Throttle use cases
- ✅Scroll events — update sticky headers or infinite scroll at a steady rate
- ✅Mouse move / drag — track position without overwhelming the browser
- ✅Button clicks — prevent double-submit on rapid clicks
- ✅Analytics events — send tracking data at most once per second
The decision rule
Ask yourself: do I need the final value (debounce) or do I need regular updates during the action (throttle)? If you only care about what the user ended up with, debounce. If you need to respond while they're still doing it, throttle.
Common Mistakes
Creating a new debounced function on every render
In React, if you define the debounced function inside the component body without memoization, a new function is created on every render — the timer resets constantly and debounce never works.
✅Use `useMemo` or `useRef` to persist the debounced function across renders, or define it outside the component.
Not cleaning up on unmount
If a component unmounts while a debounced call is pending, the callback fires on a stale component — causing memory leaks or state-update-on-unmounted-component warnings.
✅Cancel the pending debounce in a cleanup function (useEffect return) or use an AbortController.
Why Interviewers Ask This
This question tests whether you understand performance optimization at the event-handling level. Interviewers want to see that you know when each technique is appropriate, can implement them from scratch (demonstrating closure and timer knowledge), and understand the real-world impact on UX and network usage. It also reveals whether you think about edge cases like cleanup and React re-renders.
Quick Revision Cheat Sheet
Debounce: Waits until events stop, then fires once — "wait for silence"
Throttle: Fires at most once per interval — "steady drip"
Debounce for: Search input, resize end, auto-save, form validation
Throttle for: Scroll, drag, mousemove, rate-limited API calls
Key mechanism: Debounce uses clearTimeout + setTimeout; throttle uses a boolean flag or timestamp