Debounce vs Throttle: Rate-Limiting Event Handlers with Lodash
Choose between Lodash debounce and throttle for event handlers, with leading/trailing options, cancel/flush cleanup, and bundle-size cautions.
04 Dec 2025, 00:50 UTC

A search field that requests on every keystroke
A search input that calls your API on every keystroke can overwhelm the server and make the page feel sluggish. The same pattern appears in scroll listeners, window resize handlers, and rapid button clicks. Lodash provides two rate-limiting tools—_.debounce and _.throttle—but they are not interchangeable. Choosing the wrong one either drops user input or still floods your endpoint.
_.debounce delays a function call until a quiet period passes. _.throttle guarantees a function runs at most once per interval. Both are higher-order wrappers: you call them with a function and a wait time, and they return a new function you attach to the event.
Use debounce when the user needs to stop first
Debounce is the right fit for search fields, autocomplete, and "save after typing stops" behavior. By default it fires on the trailing edge: after the user stops calling the wrapped function for the full wait period. Setting leading: true fires immediately on the first call, while trailing: false suppresses the final call.
One frequently missed rule: maxWait only works when trailing: true. If you set trailing: false, Lodash ignores maxWait, so a user who keeps typing can delay the call indefinitely. In Lodash 4.17 and later, the returned function exposes cancel() to clear a pending timer and flush() to invoke it immediately.
Use throttle when you need a steady heartbeat
Throttle suits scroll and resize handlers where you want regular updates rather than waiting for the user to stop. Both leading and trailing default to true, so the first call fires immediately and the last call fires at the end of the window if needed. Unlike debounce, throttle does not reset its window on every call; it enforces a fixed maximum frequency.
Worked example: debounced search with cleanup
This example attaches a debounced search handler to an input. It imports only the Lodash function needed, which keeps the bundle smaller than importing the full lodash library.
import debounce from 'lodash/debounce';
const input = document.querySelector('#search');
const runSearch = debounce(async (query) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const results = await response.json();
// update UI with results
}, 300);
input.addEventListener('input', (event) => {
runSearch(event.target.value);
});
// Call this when removing the listener or unmounting a component:
// runSearch.cancel();
If you are using React, return runSearch.cancel() from a useEffect cleanup function. React 18 StrictMode mounts effects twice in development, so skipping cleanup can leave duplicate timers running. TypeScript types the wrapped result as DebouncedFunc, which includes cancel(), flush(), and pending().
Limits and bundle-size trade-offs
Individual imports such as lodash/debounce are much smaller than the full lodash import, but exact sizes depend on your bundler and tree-shaking configuration. Verify the difference in your own build rather than assuming a number.
To inspect your bundle, run this in your project root:
npx webpack-bundle-analyzer --mode=production
Required permissions: none. Expected checks: open the treemap and confirm Lodash appears only as the slices you imported. Risks: a production build takes longer than development, and the analyzer is a development dependency.
Timers also drift in background tabs because they rely on setTimeout. For animation-synchronized work, prefer requestAnimationFrame over Lodash throttle.
Decision checklist
- Wait until the user stops typing or clicking: use
_.debouncewith trailing edge. - Need an immediate first response: add
leading: true. - Need a hard ceiling on delay while typing continues: use
_.debouncewithtrailing: trueandmaxWait. - Scroll, resize, or steady progress updates: use
_.throttle. - Component unmounts or listener removes: call
cancel()to clear pending timers.
Start with the smallest import that matches your event pattern, add cleanup for component lifecycles, and confirm behavior with fake timers in unit tests before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.