How to stop a search box from firing 20 requests at once.

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.

Learn about reducing technical debt

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

javascript
// 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.

javascript
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:

javascript
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:

javascript
// 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