Some browser events fire far more often than your code needs to react. A scroll handler can run dozens of times per second. A search box fires on every keystroke. If each event triggers a network call or a heavy calculation, the page slows down and the server takes needless load.
Debounce and throttle are two ways to limit how often a function runs. They are often confused. This guide explains the difference and shows when to use each.
The Short Version
- Debounce waits until the events stop. The function runs once, after a quiet period.
- Throttle runs the function at a steady rate while events keep coming. At most once per interval.
| Debounce | Throttle | |
|---|---|---|
| Runs | After events stop for N ms | At most once every N ms |
| During a burst | Nothing, until it ends | Regularly |
| Number of calls for a 1-second burst, N = 200 ms | 1 | About 5 |
| Good for | Final value matters | Ongoing feedback matters |
What Is Debounce?
Debounce delays a function until a set time has passed since the last call. Each new call resets the timer.
function debounce(fn, delay) {
let timer
const debounced = (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
debounced.cancel = () => clearTimeout(timer)
return debounced
}
Typing "react" quickly produces five keystrokes. With a 300 ms debounce, the function runs once, 300 ms after the last letter.
Example: Search as You Type
const search = debounce(async (query) => {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
render(await res.json())
}, 300)
input.addEventListener('input', (event) => search(event.target.value.trim()))
Without the debounce, typing "react" sends five requests. With it, one.
What Is Throttle?
Throttle lets a function run at most once per interval and ignores the calls in between.
function throttle(fn, interval) {
let last = 0
return (...args) => {
const now = Date.now()
if (now - last >= interval) {
last = now
fn(...args)
}
}
}
This version runs on the first call and then at most once per interval. It drops the final call if it falls inside the interval, which matters if you need the last value. A more complete version, such as the one in Lodash, can also run on the trailing edge.
Example: Scroll Position
const updateProgress = throttle(() => {
const scrolled = window.scrollY / (document.body.scrollHeight - window.innerHeight)
bar.style.width = `${Math.min(scrolled, 1) * 100}%`
}, 100)
window.addEventListener('scroll', updateProgress, { passive: true })
The progress bar updates ten times per second while scrolling, which looks continuous and costs little.
Which One Should You Use?
Ask what you need during the burst.
| Situation | Choice | Reason |
|---|---|---|
| Search suggestions | Debounce | Only the final text matters |
| Form validation as you type | Debounce | Validate when typing pauses |
| Auto-save a draft | Debounce | Save after the user stops |
| Window resize layout recalculation | Debounce | Compute once at the final size |
| Scroll progress or sticky headers | Throttle | Needs regular updates |
| Mouse move or drag tracking | Throttle | Needs ongoing position |
| Infinite scroll check | Throttle | Check regularly, not on every pixel |
| Button double-click protection | Throttle (leading edge) | Run the first click, ignore the rest |
If the answer to "do I only care about the last value?" is yes, debounce. If it is "I need updates while it happens", throttle.
Using Them in React
Creating the function inside a component body makes a new debounced function on every render, and the timer state is lost. Keep one instance stable.
import { useEffect, useMemo, useState } from 'react'
function SearchBox() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const search = useMemo(
() =>
debounce(async (text) => {
const res = await fetch(`/api/search?q=${encodeURIComponent(text)}`)
setResults(await res.json())
}, 300),
[],
)
useEffect(() => () => search.cancel(), [search])
return (
<input
value={query}
onChange={(event) => {
setQuery(event.target.value)
search(event.target.value)
}}
/>
)
}
Three details matter here:
useMemowith an empty dependency array keeps the same debounced function across renders.- The cleanup cancels a pending call when the component unmounts, which prevents a state update on an unmounted component.
- The input stays controlled and updates instantly. Only the network call is delayed.
A second option is to debounce the value, not the function. A useDebouncedValue hook returns the value after it has been stable for N ms, and a normal useEffect reacts to it. This reads more naturally in React code.
Handling Out-of-Order Responses
Debouncing reduces requests but does not guarantee the responses arrive in order. A slow response for an older query can overwrite a newer one. Cancel the previous request with an AbortController:
let controller
const search = debounce(async (query) => {
controller?.abort()
controller = new AbortController()
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
render(await res.json())
} catch (error) {
if (error.name !== 'AbortError') throw error
}
}, 300)
Choosing the Delay
There is no universal number. These ranges are a starting point:
- Debounce for typing: 250 to 400 ms. Shorter feels responsive and sends more requests.
- Throttle for scroll: 50 to 100 ms. Longer starts to look choppy.
- Resize: 150 to 250 ms.
For animations tied to scrolling, requestAnimationFrame is often better than a fixed interval, because it matches the screen refresh rate.
Common Mistakes
- Creating a new debounced function on every call. Each one has its own timer, so nothing is debounced.
- Debouncing when you need regular updates. A scroll indicator with debounce only moves after you stop scrolling.
- Forgetting to cancel on unmount. A pending call can run after the component is gone.
- Delays that are too long. A 1-second debounce on a search box feels broken.
- Using either one to hide a slow function. If a handler is slow, fix it or move it to a worker.
Summary
Debounce runs a function once after activity stops. Throttle runs it regularly while activity continues. Pick debounce when only the final value matters, such as search and auto-save, and throttle when you need updates during the activity, such as scroll and drag.
