ForgeFrontend — Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

Debounce vs Throttle in JavaScript

Short answer

Both limit how often a function runs during rapid events. Debounce waits until the activity stops and then runs once — right for search-as-you-type. Throttle runs at most once per interval no matter how many events arrive — right for scroll, resize and mousemove.

DebounceThrottle
RunsOnce, after the burst stopsAt a fixed maximum rate
During continuous eventsNever runsRuns every interval
Guarantees a final callYesOnly with a trailing call
Timer behaviourReset on every eventIgnore events until it expires
Feels like"Wait until they're done""At most once every N ms"
Typical delay300–500ms100–250ms
Good forSearch input, autosave, validationScroll, resize, mousemove, drag
Wrong forAnything needing progress feedbackAnything that should collapse to one call
Both cap how often the work happens; they disagree on when.

The difference in one picture

Imagine a user typing eight characters over two seconds, and a function you want to limit. Debounced at 400ms, nothing happens while they type; 400ms after the last keystroke, one call. Throttled at 400ms, a call goes out roughly every 400ms during the typing — about five calls — regardless of when it stops.

Same events, two behavioursjavascript
1// keystrokes at t = 0, 100, 200, 300, 900, 1000 (ms)
2
3debounce(fn, 400)
4// → one call at t = 1400
5//   (each keystroke restarted the timer; the last one at 1000 won)
6
7throttle(fn, 400)
8// → calls at t = 0, 400, 900
9//   (fires immediately, then at most once per 400ms window)

That is the whole distinction, and the choice follows from one question: does the intermediate activity matter? For a search box, only the final query matters, so collapsing the burst into one call is exactly right. For a scroll position indicator, the intermediate values are the entire point — a debounced version would show nothing until the user stopped scrolling.

Implementing debounce

Debounce keeps one timer in a closure. Every call clears the pending timer and schedules a new one, so only a call that survives the full delay without being interrupted actually runs. The details that separate a passing implementation from a good one are preserving the arguments, preserving `this`, and returning a way to cancel.

Debounce, with the details interviewers look forjavascript
1function debounce(fn, delay) {
2  let timer = null;
3
4  function debounced(...args) {
5    clearTimeout(timer);
6    timer = setTimeout(() => {
7      timer = null;
8      fn.apply(this, args);        // preserve `this` and the args
9    }, delay);
10  }
11
12  debounced.cancel = () => { clearTimeout(timer); timer = null; };
13  return debounced;
14}
15
16const log = debounce((q) => console.log("search:", q), 300);
17log("r"); log("re"); log("rea"); log("react");
18// → search: react     one call, 300ms after the last one
Leading edge: run immediately, then ignore the rest of the burstjavascript
1function debounce(fn, delay, { leading = false } = {}) {
2  let timer = null;
3
4  return function (...args) {
5    const callNow = leading && timer === null;
6    clearTimeout(timer);
7    timer = setTimeout(() => {
8      timer = null;
9      if (!leading) fn.apply(this, args);
10    }, delay);
11    if (callNow) fn.apply(this, args);
12  };
13}
14
15const save = debounce(submit, 1000, { leading: true });
16save(); save(); save();
17// → one call, immediately — later calls inside the window are dropped.
18//   This is the correct shape for a submit button guard.

Implementing throttle

Throttle is the mirror image: instead of resetting a timer, it records when it last ran and ignores everything until the interval has elapsed. There are two common implementations — timestamp-based and timer-based — and the difference between them is whether a trailing call happens after the last event.

Timestamp version: leading edge onlyjavascript
1function throttle(fn, limit) {
2  let last = 0;
3
4  return function (...args) {
5    const now = Date.now();
6    if (now - last >= limit) {
7      last = now;
8      fn.apply(this, args);
9    }
10  };
11}
12
13const onScroll = throttle(() => console.log(window.scrollY), 200);
14// → fires immediately on the first scroll event, then at most
15//   every 200ms. Note: nothing fires after scrolling stops, so
16//   the final position may never be reported.
With a trailing call, so the last event is never lostjavascript
1function throttle(fn, limit) {
2  let last = 0;
3  let timer = null;
4
5  return function (...args) {
6    const now = Date.now();
7    const remaining = limit - (now - last);
8
9    if (remaining <= 0) {
10      clearTimeout(timer);
11      timer = null;
12      last = now;
13      fn.apply(this, args);
14    } else if (!timer) {
15      timer = setTimeout(() => {
16        last = Date.now();
17        timer = null;
18        fn.apply(this, args);
19      }, remaining);
20    }
21  };
22}
23// → fires on the leading edge AND once more after the final event,
24//   which is what you want for a scroll indicator that must end
25//   on the true final position

This is 1 of 80+ questions in the JavaScript Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the JavaScript Interview Kit → ₹299

Choosing between them

The decision is almost always obvious once you ask what the intermediate calls are for. If they are noise on the way to a final answer, debounce. If each one carries information the user should see, throttle.

CaseUseWhy
Search-as-you-typeDebounceOnly the final query is worth a request
Autosave a draftDebounceSave once the user pauses, not per keystroke
Form validation on inputDebounceDo not shout at someone mid-word
Scroll position indicatorThrottleIntermediate positions are the content
Infinite scroll triggerThrottleMust react during the scroll, not after
Window resize re-layoutThrottle + trailingUpdate during, settle correctly after
Mousemove drag previewThrottleEvery frame matters; cap it to the frame rate
Button double-click guardDebounce, leadingRun the first, drop the rest
Rate-limiting an API callThrottleThe cap is the requirement
Common cases and the reason, not just the answer.

The React trap that breaks both

This is where most real bugs live. Calling debounce inside a component body creates a new debounced function on every render, each with its own fresh timer, so nothing ever survives long enough to fire. It looks correct and does nothing, which makes it a favourite interview follow-up.

The broken version and the fixjsx
1// Broken — a new debounced function on every keystroke
2function Search() {
3  const [q, setQ] = useState("");
4  const run = debounce((v) => fetchResults(v), 400);
5
6  return <input onChange={e => { setQ(e.target.value); run(e.target.value); }} />;
7  // → each render makes a new closure with a new timer,
8  //   so the previous timer is never cleared and every
9  //   keystroke eventually fires. Zero debouncing.
10}
11
12// Fixed — one stable debounced function for the component's life
13function Search() {
14  const [q, setQ] = useState("");
15  const run = useMemo(() => debounce(v => fetchResults(v), 400), []);
16
17  useEffect(() => () => run.cancel(), [run]);   // cancel on unmount
18
19  return <input onChange={e => { setQ(e.target.value); run(e.target.value); }} />;
20  // → one timer across renders, and no setState after unmount
21}

Note the cleanup. A pending debounced callback that fires after the component unmounts will try to update state on something that no longer exists — historically a warning, and always a sign of a leak. Cancelling on unmount is the part people forget, and mentioning it unprompted reads well.

The debounced-value pattern, which avoids the whole problemjsx
1function useDebouncedValue(value, delay = 400) {
2  const [debounced, setDebounced] = useState(value);
3
4  useEffect(() => {
5    const id = setTimeout(() => setDebounced(value), delay);
6    return () => clearTimeout(id);   // reset on every change
7  }, [value, delay]);
8
9  return debounced;
10}
11
12function Search() {
13  const [q, setQ] = useState("");
14  const query = useDebouncedValue(q, 400);
15
16  useEffect(() => { if (query) fetchResults(query); }, [query]);
17
18  return <input value={q} onChange={e => setQ(e.target.value)} />;
19  // → the input stays instant; only the derived value lags.
20  //   No stale closures, no cancel to remember.
21}

Debouncing the value rather than the callback is usually the better React answer. The effect's own cleanup handles cancellation, the input remains fully controlled and responsive, and there is no long-lived function holding stale props. React 18's useDeferredValue solves an adjacent problem — keeping the input responsive while an expensive render catches up — but it does not delay a network request, so the two are not interchangeable.

What the platform already gives you

Before hand-rolling either, check whether the problem has a purpose-built API. Several of the classic use cases now have better answers than a timer, and knowing them is a stronger signal than knowing the implementations.

  • requestAnimationFrame — for anything visual driven by scroll or mousemove; it throttles to the display's refresh rate by definition.
  • IntersectionObserver — for infinite scroll and lazy loading, instead of a throttled scroll handler that measures positions.
  • ResizeObserver — for reacting to element size, instead of a throttled window resize listener.
  • AbortController — to cancel the in-flight request when a new search starts, which debouncing alone does not do.
  • CSS scroll-driven animations — for progress bars and parallax, with no JavaScript in the loop at all.
  • The scrollend event — for reacting once scrolling has finished, which is a debounce the browser implements for you.
Debounce plus abort: the complete search patternjavascript
1let controller;
2
3const search = debounce(async (q) => {
4  controller?.abort();                 // drop the previous request
5  controller = new AbortController();
6
7  try {
8    const res = await fetch(`/api/search?q=${q}`, { signal: controller.signal });
9    render(await res.json());
10  } catch (e) {
11    if (e.name !== "AbortError") throw e;
12  }
13}, 300);
14// → debounce reduces how many requests start; abort makes sure a
15//   slow earlier response cannot overwrite a newer one. You need
16//   both — debouncing alone does not fix out-of-order responses.

Getting the delay right

The numbers matter more than people expect. Around 300ms is the usual starting point for a search debounce: long enough to collapse a burst of typing, short enough that results feel immediate. Below about 150ms you are barely reducing requests; above 600ms the interface starts to feel unresponsive and users begin retyping.

For throttle, tie the interval to what the output is for. A visual update should be throttled with requestAnimationFrame rather than a millisecond value, so it matches the display. A network call driven by scroll should be throttled far more conservatively — 500ms or more — because the constraint is your server, not the screen.

Which should I use for a search box?

Debounce, at roughly 300ms, combined with an AbortController to cancel the previous request. Debouncing reduces how many requests start; aborting prevents a slow earlier response from arriving after a newer one and overwriting the results.

Can you debounce and throttle the same function?

Yes, and Lodash's throttle is implemented as a debounce with a maxWait option — a debounce that is forced to fire at least every N milliseconds. That combination is useful when you want the collapsing behaviour but cannot tolerate an unbounded wait during continuous input.

Why does my debounced function fire on every keystroke in React?

Because it is being recreated on each render, so each call schedules a timer on a brand-new closure and nothing ever cancels the previous one. Wrap the debounced function in useMemo with an empty dependency array, or debounce the value with a useEffect-based hook instead.

Does debouncing cancel in-flight requests?

No. It only delays when a request starts. Once a fetch has gone out, a later debounced call does nothing about it, so responses can still arrive out of order. Pair debounce with AbortController, or key the results by query and ignore any response that no longer matches the current input.

Should I use Lodash or write my own?

In production, use a library — Lodash handles leading and trailing edges, maxWait, cancel and flush, and the edge cases are genuinely fiddly. In an interview, write your own: the request is a test of closures and timers, and reaching for a package is not the answer they want.

Frequently asked questions

What is the difference between debounce and throttle?
Debounce waits until the events stop arriving and then runs the function once. Throttle runs the function at most once per interval while events keep arriving. Debounce collapses a burst into a single call; throttle spreads the calls out at a fixed maximum rate.
When should I use debounce?
When only the final state of a rapid sequence matters: search-as-you-type, autosaving a draft, validating a field while typing, or guarding a submit button against double clicks. The intermediate values are noise on the way to one answer.
When should I use throttle?
When the intermediate events carry information the user should see: scroll position indicators, infinite-scroll triggers, drag previews, mousemove effects and window resize handling. Debouncing these makes the interface feel frozen and then jumpy.
How do you implement debounce in JavaScript?
Keep a timer id in a closure. On each call, clear the pending timer and schedule a new one for the delay; only a call that survives the full delay runs the function. Use fn.apply(this, args) so arguments and the receiver are preserved, and expose a cancel method that clears the timer.
How do you implement throttle in JavaScript?
Store the timestamp of the last run in a closure and ignore calls until the interval has elapsed. To also guarantee a trailing call, schedule a timeout for the remaining time when a call arrives inside the window, so the final event is not lost.
What are leading and trailing edges?
The leading edge is running at the start of a burst of events, the trailing edge at the end. Debounce is trailing by default and throttle is usually both. Lodash exposes them as options, and choosing correctly is often the difference between a responsive interface and a laggy one.
Why does my debounce not work in React?
Because calling debounce during render creates a new debounced function every render, each with its own timer, so nothing is ever cancelled and every call eventually fires. Wrap it in useMemo with an empty dependency array, or debounce the value with a useEffect-based hook.
How do I write a useDebounce hook?
Hold the debounced value in state and run a useEffect that sets it after a timeout, returning a clearTimeout cleanup. Because the effect re-runs on every value change, the cleanup cancels the previous timer automatically — which is the debounce, with no stale closures.
What delay should I use for a search input?
About 300ms is the common starting point. Below roughly 150ms you barely reduce the number of requests; above 600ms results start to feel late and users retype. Tune it against your actual response time rather than treating it as a constant.
Does debounce prevent race conditions?
No. It reduces how many requests start, but once two are in flight the slower one can still resolve last and overwrite newer results. Cancel the previous request with AbortController, or discard any response whose query no longer matches the current input.
Is requestAnimationFrame better than throttle for scroll?
For anything visual, yes. rAF runs your callback once per frame, matching the display's refresh rate exactly, so you never do work that is thrown away before it is painted. Use a millisecond throttle when the constraint is a network or CPU budget rather than the screen.
What is the difference between debounce and useDeferredValue?
Debounce delays when work starts, on a timer you choose. useDeferredValue lets React render an expensive component with a stale value while the fresh one is prepared, keeping the input responsive. It does not delay a network request, so it is not a replacement for debouncing a search.

This is 1 of 80+ questions in the JavaScript Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the JavaScript Interview Kit → ₹299
Written by Arun Karthikeyan · Last updated

Full kit

JavaScript Interview Kit · ₹299

Get it →