cf.completefrontend
ALGORITHMS IN FRONTEND WORK · CORE

Debounce and throttle

Limit how often a function runs during a burst of events: debounce waits for a quiet moment, throttle runs at most once per time window.

Time O(1) per callSpace O(1)5 practice problemsStep-through picture

The idea

Some events fire very often. Typing fires input on every key, scrolling fires scroll many times a second, and resizing a window fires resize constantly. If each event starts a search request or a heavy layout, the page slows down and the server gets flooded.

Debounce waits until the events stop. Each call cancels the timer from the previous call and starts a new one for wait milliseconds. The function only runs when wait milliseconds pass with no new call, so a burst of typing produces one search, after the user pauses. This is the trailing edge: running at the end of a burst.

Throttle runs the function at most once every wait milliseconds. The first call runs right away (the leading edge), and calls during the next wait milliseconds are ignored. A long scroll gets steady updates, about once per window, instead of none until it ends. Many libraries let you choose leading, trailing, or both edges for either tool.

An everyday example

An elevator door and a bus timetable

An elevator door is a debounce: each person who walks in resets the door timer, and the door only closes once nobody has come in for a few seconds. A bus with a fixed timetable is a throttle: it leaves at most once every ten minutes, no matter how many people arrive; the first person after a departure gets the next bus right away if it's been ten minutes, and everyone else in that window waits.

How the example maps to the code
In the exampleIn the code
A person walking into the elevatorA call to the debounced function
Resetting the door timerclearTimeout(timer) then a new setTimeout
The door finally closingThe timeout fires and runs fn
The bus leavingfn runs and last = now
Too soon since the last busnow - last < wait, so the call is skipped

From the slow way to debounce and throttle

  1. Start with the obvious way: run the handler on every event. Typing laptop fires 6 input events and sends 6 search requests. The answers can come back out of order, so the page may show results for lap after the results for laptop. A scroll handler that measures layout can run many times per frame.
  2. There are two ways to run less. Wait until the events stop, then run once: that is debounce, and it fits work that only needs the final value, like a search. Or run at most once per time window while events keep coming: that is throttle, and it fits steady feedback, like a scroll position.
  3. Debounce needs to know "has it been quiet for wait ms?", but code can't see the future. So plan instead: every call schedules a run wait ms later and cancels the run the previous call planned. Only the newest plan survives, and it only fires if no call arrived in time. All the state is one timer variable kept in a closure.
  4. Throttle needs even less: remember last, the time of the last run. On each call, if now - last >= wait, run and set last = now; otherwise drop the call. Two small closures wrapped around fn are debounce and throttle.

How it works

  1. Debounce keeps a timer. Every call first cancels the old one with clearTimeout(timer).
  2. Then it starts a new timer with setTimeout for wait milliseconds, remembering the latest arguments.
  3. If no call arrives before the timer ends, the timer runs fn once with the latest arguments. A call that arrives in time cancels it and starts over.
  4. Throttle keeps last, the time fn last ran, starting at -Infinity so the first call always runs.
  5. On each call it reads the current time now. If now - last >= wait, it runs fn and sets last = now; otherwise it ignores the call.

Watch it run

This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.

Loading the picture…
See every step at onceThe same run, laid out side by side
Drawing every step…

The code

Point at a note to light up the lines it explains.

  1. function debounce(fn, wait) {
  2. let timer = null;
  3. return function (...args) {
  4. clearTimeout(timer);
  5. timer = setTimeout(() => {
  6. timer = null;
  7. fn.apply(this, args);
  8. }, wait);
  9. };
  10. }
  11. function throttle(fn, wait) {
  12. let last = -Infinity;
  13. return function (...args) {
  14. const now = Date.now();
  15. if (now - last >= wait) {
  16. last = now;
  17. fn.apply(this, args);
  18. }
  19. };
  20. }

How fast it is

TimeO(1) per call
Extra memoryO(1)

Each call clears and sets one timer, or compares two numbers. Only one timer or timestamp is kept, however many calls arrive.

  • Best caseO(1)

    Any call: debounce clears one timer and sets one; throttle compares two numbers.

  • Average caseO(1)

    Same for every call in a burst, however long the burst is.

  • Worst caseO(1)

    Still one clear and one set, or one comparison. Simulating a list of n call times takes O(n) in total.

Common mistakes

  • Creating a new debounced function on every render or event, such as inside a React component body without useMemo or useRef. Each one has its own timer, so nothing is ever cancelled.
  • Using an arrow function for the returned wrapper and expecting this to be the element. Use function and fn.apply(this, args) as above.
  • Using debounce for scroll-driven UI like a sticky header. Nothing happens until scrolling stops; throttle (or requestAnimationFrame) gives steady updates.
  • Forgetting the trailing call in a throttle. The last event in a burst may be skipped, so the UI shows a stale scroll position or size.

When to use it

Look for these clues in a problem:

  • Events arrive in bursts (typing, scrolling, resizing, dragging) and each one triggers expensive work.
  • The question gives call timestamps and a wait time and asks when the function actually runs.
  • It mentions search-as-you-type, autosave, rate limits, or "at most once every N ms".
  • You need to say what happens at the edges: the first call of a burst, the last one, or both.

Check yourself

Answer each one in your head first, then open it.

Calls arrive at 0, 100, and 200 with wait = 150. When do the debounced and throttled versions run?

Debounce runs once, at 350: each call pushed the plan back, and the last one was at 200. Throttle runs at 0 and 200, because 100 - 0 is under 150 but 200 - 0 isn't.

Why does the wrapper use function and fn.apply(this, args) instead of an arrow function?

An arrow function would take this from where it was written, not from the element the handler is attached to. function gets the caller's this, and apply passes it on to fn.

Why does debounce(search, 300) written inside a React component body never seem to wait?

Each render creates a new debounced function with its own timer. A new call never cancels the old timer, so every keystroke still runs search.

During 5 seconds of nonstop scroll events, at most how many times can a throttle with wait = 200 run?

26: once at the start and then once per 200 ms window, if events land exactly as each window ends. In practice it's about 25.

Practice problems

Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.

Go further

You don't need these to start practicing. Open one when you want more.

Why it works

The reasoning that makes it always right

Debounce keeps one rule: at most one timer is waiting, and it belongs to the newest call, due wait ms after it. Each call keeps the rule by cancelling the waiting timer with clearTimeout(timer) and starting its own. So when a timer fires, it must be the newest call's timer, and no call came in the wait ms after it. That is exactly "the burst is over", so the run happens once per burst, with the latest args.

Throttle keeps a different rule: last is the time of the most recent run, and runs are always at least wait ms apart. A run only happens when now - last >= wait, and it updates last at once. The first call always runs because last starts at -Infinity. Calls inside a window are dropped, not delayed, which is why plain throttle can miss the final event of a burst.

Both rely on JavaScript running one piece of code at a time. Two calls can't run the wrapper at the same moment, so timer and last are never read and written by two calls at once.

Going deeper

3 more ideas

Why debounce runs exactly once per burst

Only one timer is ever waiting, because each call cancels the previous one. The waiting timer belongs to the newest call. It survives only if wait milliseconds pass with no newer call, which is the definition of the burst being over. So each burst ends with exactly one run, wait milliseconds after its last call.

Leading, trailing, and maxWait

A leading-edge debounce runs on the first call of a burst and then ignores calls until things are quiet for wait ms; it suits buttons that must not be double-submitted. A throttle with a trailing edge also runs once more at the end of the window if calls were ignored, so the final scroll position isn't lost. Lodash's debounce has a maxWait option: even in an endless burst, the function runs at least once every maxWait ms. Lodash's throttle is built on debounce with maxWait equal to wait.

How fast it is

Each call does O(1) work: clear one timer and set another, or compare two times. The state is one timer or one timestamp, so memory is O(1). Simulating a list of n call times is O(n).

Work it by hand

Every step in a table, with why

Six calls with wait = 150, sent to both a debounced and a throttled function. Timers fire between calls, and the call at 560 starts a new burst just after a run.

call times = [0, 60, 130, 170, 400, 560], wait = 150, arguments `q0` to `q5`

StepTimeEventDebounce timer dueThrottle lastWhat happensWhy
10call q01500throttle runs q0Debounce plans a run at 150. Throttle's last is -Infinity, so now - last is Infinity and it runs right away.
260call q12100throttle skipsDebounce cancels the plan for 150 and plans 210. Throttle: 60 - 0 = 60, less than 150, so the call is dropped.
3130call q22800throttle skipsThe debounce plan moves again. 130 - 0 is still under 150.
4170call q3320170throttle runs q3170 - 0 = 170 is at least 150, so throttle runs and sets last = 170.
5320timer firesnone170debounce runs q3No call came within 150 ms of 170, so the planned run happens with the latest arguments, q3. timer goes back to null.
6400call q4550400throttle runs q4A new burst. 400 - 170 = 230, so throttle runs too.
7550timer firesnone400debounce runs q4Quiet for 150 ms after 400, so debounce runs q4.
8560call q5710560throttle runs q5Only 10 ms after the debounced run, but that doesn't matter: a new plan starts. 560 - 400 = 160, so throttle runs.
9710timer firesnone560debounce runs q5The last plan survives, and no calls are left.

Debounce ran 3 times, at 320, 550, and 710, each 150 ms after the last call of a burst. Throttle ran 4 times, at 0, 170, 400, and 560. Both dropped q1 and q2.

Compared with other ways

When to pick something else
ApproachTimeExtra memoryChoose it when
Debounce and throttleO(1)O(1)Bursts of events trigger costly work. Debounce when only the final value matters, throttle when the user needs steady updates.
Run on every eventO(1)O(1)The handler is cheap, like updating a character count, so running it every time costs nothing noticeable.
requestAnimationFrame throttleO(1)O(1)The work changes what's on screen, like moving a dragged element. Once per frame is exactly as often as the user can see.
Queue every event and send in batchesO(n)O(n)No event may be dropped, like analytics events, but sending each one alone is too costly. Collect them and send every few seconds.

Extra memory: O(1) per wrapped function: one timer id and the latest args for debounce, one number for throttle.

How the steps grow

1101001k10k100k1M116324864input size n
Steps as the input grows, for Debounce and throttle and the other approaches, on a scale where each line up is 10 times more steps.
  • Debounce and throttle, Run on every event, requestAnimationFrame throttle O(1)
  • Queue every event and send in batches O(n)
About how many steps for bigger inputs
Approachn = 10n = 1,000n = 1,000,000
Debounce and throttle111
Run on every event111
requestAnimationFrame throttle111
Queue every event and send in batches101,0001 million

Inputs that need care

5 tricky inputs
wait = 0
Debounce still waits for a later task, so several calls made in the same synchronous run collapse into one. Throttle runs on every call, since now - last >= 0 is always true.
The system clock jumps backward
Date.now() follows the system clock. If it moves back, now - last can be negative, and throttle drops every call until the clock passes last + wait again. performance.now() only moves forward, so it is safer.
The tab is in the background
Browsers slow down timers in hidden tabs, often to about once per second or less. A debounced autosave may run much later than wait ms; save on visibilitychange too if it matters.
The component that owns the handler is removed while a debounce is waiting
The timer still fires and calls fn on something that is gone. Add a cancel method that runs clearTimeout(timer), and call it in the cleanup, as Lodash's debounced.cancel() allows.
fn starts an async request
Debounce limits how many requests start, but not the order answers arrive in. Cancel the old request with an AbortController, or ignore answers that aren't for the latest query.

Variations and follow-ups

Twists interviewers add

Leading-edge debounce

Run on the first call of a burst, then ignore calls until it has been quiet for wait ms. It reacts instantly and suits a submit button that must not fire twice from a double click.

Throttle with a trailing call

When a call is dropped, remember its args and plan one run at last + wait. The final scroll position or size is then never lost. Lodash's throttle runs on both the leading and the trailing edge by default.

maxWait

A debounce with maxWait runs at least once every maxWait ms even if the burst never ends, which keeps autosave working during long typing sessions. Lodash builds its throttle from debounce with maxWait equal to wait.

Throttle to animation frames

Plan at most one requestAnimationFrame at a time and do the work inside it. Visual updates then run once per frame, matched to the screen's refresh rate, instead of on a fixed number of milliseconds.

In an interview

Spot it, explain it, answer follow-ups
  • Spot it from "fires too often", "after the user stops typing", "at most once every N ms", or a list of call times plus a wait. Decide early whether the question wants the end of a burst (debounce) or a steady rate (throttle).
  • Explain the state first: debounce keeps one timer, throttle keeps one last, both in a closure shared by every call. Then say which edge each one runs on.
  • Test by hand by drawing a timeline like the trace: a burst, a gap longer than wait, then a call just after a run. Write down when each version runs.
  • Expect follow-ups: add cancel, add leading and trailing options, or use debounce in a React component. For React, keep one debounced function across renders with useMemo or useRef, and cancel it in the effect cleanup.

Where frontend code uses it

The same idea in real UI code
  • Search boxes debounce the input event so a request is sent only after the user pauses typing.
  • Scroll and resize handlers are throttled, or tied to requestAnimationFrame, so layout work runs at most once per frame.
  • Lodash's debounce and throttle support leading, trailing, and maxWait options; autosave in editors often uses debounce with a maxWait so long sessions still save.

Related algorithms