How to stop a search box from firing 20 requests at once.
AI is Escalating Tech Debt
AI writes code fast, but without architecture context it creates hidden “dark matter debt” that compounds inside your codebase.
A search input fires on every keystroke. Type “javascript” and that is 10 separate API calls before the user even finishes the word. Most of those requests are wasted the moment the next one fires. Debouncing waits until the user stops typing for a brief moment before actually running the function. Ten lines of plain JavaScript, no library required.
The problem
// fires on every single keystroke
input.addEventListener('input', (e) => {
searchAPI(e.target.value); // 10 calls for "javascript"
});
The debounce function
It wraps any function and delays it. Every new call cancels the pending one and restarts the timer. The wrapped function only actually runs once the calls stop coming in.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
Use it by wrapping the function you want to delay:
const debouncedSearch = debounce(searchAPI, 300);
input.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// "javascript" typed at normal speed:
// 0 API calls until 300ms after the last keystroke
// then exactly 1 call, with the final value
Type the whole word and only one request fires, 300 milliseconds after the last keystroke. Ten wasted calls become one useful one.
This is not just for search. Window resize handlers, scroll event listeners, form validation on every keystroke, autosave features. Anything that fires rapidly and only needs to run once the activity settles is a candidate for debounce. 300ms works well for search; resize and scroll handlers often use 100 to 150ms.
Debounce vs throttle
These two get confused constantly. Debounce waits for activity to stop. Throttle runs at a fixed interval no matter how often the event fires:
// debounce: waits for silence, runs once at the end
// good for: search, autosave, form validation
// throttle: runs at most once every N ms, regardless
// good for: scroll position tracking, mouse move handlers
function throttle(fn, limit) {
let waiting = false;
return function (...args) {
if (!waiting) {
fn(...args);
waiting = true;
setTimeout(() => waiting = false, limit);
}
};
}
Use debounce when you only care about the final value. Use throttle when you need regular updates throughout the activity, like updating a scroll progress bar.
Why it matters
- Fewer API calls, lower cost. A debounced search can cut request volume by 80 to 90% with zero change in user experience.
- Smoother UI. Resize and scroll handlers that run on every event can cause visible jank. Debouncing or throttling them keeps the page responsive.
- Ten lines, no dependency. No need to pull in a utility library just for this. Copy the function above into any project.
If your app has a search box, a resize listener, or anything tied to rapid-fire events, debounce or throttle is very likely missing and worth adding today.
Happy coding!
Marko