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.
O(1) per callSpace O(1)5 practice problemsStep-through pictureThe 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.
| In the example | In the code |
|---|---|
| A person walking into the elevator | A call to the debounced function |
| Resetting the door timer | clearTimeout(timer) then a new setTimeout |
| The door finally closing | The timeout fires and runs fn |
| The bus leaving | fn runs and last = now |
| Too soon since the last bus | now - last < wait, so the call is skipped |
From the slow way to debounce and throttle
- Start with the obvious way: run the handler on every event. Typing
laptopfires 6inputevents and sends 6 search requests. The answers can come back out of order, so the page may show results forlapafter the results forlaptop. A scroll handler that measures layout can run many times per frame. - 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.
- Debounce needs to know "has it been quiet for
waitms?", but code can't see the future. So plan instead: every call schedules a runwaitms 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 onetimervariable kept in a closure. - Throttle needs even less: remember
last, the time of the last run. On each call, ifnow - last >= wait, run and setlast = now; otherwise drop the call. Two small closures wrapped aroundfnare debounce and throttle.
How it works
- Debounce keeps a
timer. Every call first cancels the old one withclearTimeout(timer). - Then it starts a new
timerwithsetTimeoutforwaitmilliseconds, remembering the latest arguments. - If no call arrives before the timer ends, the timer runs
fnonce with the latest arguments. A call that arrives in time cancels it and starts over. - Throttle keeps
last, the timefnlast ran, starting at-Infinityso the first call always runs. - On each call it reads the current time
now. Ifnow - last >= wait, it runsfnand setslast = 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.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function debounce(fn, wait) {let timer = null;return function (...args) {clearTimeout(timer);timer = setTimeout(() => {timer = null;fn.apply(this, args);}, wait);};}function throttle(fn, wait) {let last = -Infinity;return function (...args) {const now = Date.now();if (now - last >= wait) {last = now;fn.apply(this, args);}};}
How fast it is
| Time | O(1) per call |
|---|---|
| Extra memory | O(1) |
Each call clears and sets one timer, or compares two numbers. Only one timer or timestamp is kept, however many calls arrive.
- Best case
O(1)Any call: debounce clears one timer and sets one; throttle compares two numbers.
- Average case
O(1)Same for every call in a burst, however long the burst is.
- Worst case
O(1)Still one clear and one set, or one comparison. Simulating a list of
ncall times takesO(n)in total.
Common mistakes
- Creating a new debounced function on every render or event, such as inside a React component body without
useMemooruseRef. Each one has its own timer, so nothing is ever cancelled. - Using an arrow function for the returned wrapper and expecting
thisto be the element. Usefunctionandfn.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.
0 of 5 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo 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`
| Step | Time | Event | Debounce timer due | Throttle last | What happens | Why |
|---|---|---|---|---|---|---|
| 1 | 0 | call q0 | 150 | 0 | throttle runs q0 | Debounce plans a run at 150. Throttle's last is -Infinity, so now - last is Infinity and it runs right away. |
| 2 | 60 | call q1 | 210 | 0 | throttle skips | Debounce cancels the plan for 150 and plans 210. Throttle: 60 - 0 = 60, less than 150, so the call is dropped. |
| 3 | 130 | call q2 | 280 | 0 | throttle skips | The debounce plan moves again. 130 - 0 is still under 150. |
| 4 | 170 | call q3 | 320 | 170 | throttle runs q3 | 170 - 0 = 170 is at least 150, so throttle runs and sets last = 170. |
| 5 | 320 | timer fires | none | 170 | debounce runs q3 | No call came within 150 ms of 170, so the planned run happens with the latest arguments, q3. timer goes back to null. |
| 6 | 400 | call q4 | 550 | 400 | throttle runs q4 | A new burst. 400 - 170 = 230, so throttle runs too. |
| 7 | 550 | timer fires | none | 400 | debounce runs q4 | Quiet for 150 ms after 400, so debounce runs q4. |
| 8 | 560 | call q5 | 710 | 560 | throttle runs q5 | Only 10 ms after the debounced run, but that doesn't matter: a new plan starts. 560 - 400 = 160, so throttle runs. |
| 9 | 710 | timer fires | none | 560 | debounce runs q5 | The 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
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Debounce and throttle | O(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 event | O(1) | O(1) | The handler is cheap, like updating a character count, so running it every time costs nothing noticeable. |
requestAnimationFrame throttle | O(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 batches | O(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
- Debounce and throttle, Run on every event,
requestAnimationFramethrottleO(1) - Queue every event and send in batches
O(n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Debounce and throttle | 1 | 1 | 1 |
| Run on every event | 1 | 1 | 1 |
requestAnimationFrame throttle | 1 | 1 | 1 |
| Queue every event and send in batches | 10 | 1,000 | 1 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 >= 0is always true. - The system clock jumps backward
Date.now()follows the system clock. If it moves back,now - lastcan be negative, and throttle drops every call until the clock passeslast + waitagain.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
waitms; save onvisibilitychangetoo if it matters. - The component that owns the handler is removed while a debounce is waiting
- The timer still fires and calls
fnon something that is gone. Add acancelmethod that runsclearTimeout(timer), and call it in the cleanup, as Lodash'sdebounced.cancel()allows. fnstarts 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 onelast, 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 withuseMemooruseRef, and cancel it in the effect cleanup.
Where frontend code uses it
The same idea in real UI code
- Search boxes debounce the
inputevent 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
debounceandthrottlesupportleading,trailing, andmaxWaitoptions; autosave in editors often uses debounce with amaxWaitso long sessions still save.