Going DeeperPro· 40 min read

Performance Patterns: Debouncing & Throttling

Some events fire dozens of times a second — every keystroke, every scroll. Debouncing and throttling tame that flood so your code (and any API) is not overwhelmed. A real-world must and a common interview question.

What you will learn

  • Explain why rapid-fire events are a problem
  • Use debouncing to wait until activity stops
  • Use throttling to run at most once per interval

The problem: events that fire too often

Some events fire extremely often. The input event fires on every single keystroke; the scroll and resize events can fire dozens of times a second. If your handler does expensive work each time — calling an API, recalculating a layout — you flood the system, waste bandwidth, and make the page feel sluggish.

Two patterns fix this, and both are built on a timer plus a closure (the closure remembers the timer between calls):

  • Debouncing — wait until the activity stops for a moment, then run once. Good for: search-as-you-type, validating a field after the user pauses.
  • Throttling — run at most once per fixed interval, no matter how many times the event fires. Good for: scroll and resize handlers, where you want steady updates but not hundreds per second.

Debouncing — wait for the pause

The mental model: every time the event fires, you cancel the previous pending action and start the timer again. The action only actually runs once the events stop long enough for the timer to finish. Imagine a lift that waits a few seconds after the last person steps in before closing the doors.

A debounced search box — runs only after you pause
<input id="search" placeholder="Type to search...">
<p id="status">Waiting for you to type…</p>

<script>
  const status = document.getElementById("status");

  // debounce: returns a wrapped function that only runs after a quiet gap
  function debounce(fn, delay) {
    let timer;                          // remembered via closure
    return function (...args) {
      clearTimeout(timer);              // cancel the previous pending run
      timer = setTimeout(() => fn(...args), delay);  // restart the wait
    };
  }

  function search(text) {
    status.textContent = text
      ? "Searching for: " + text
      : "Waiting for you to type…";
  }

  // Only search 500ms AFTER the user stops typing
  document.getElementById("search")
    .addEventListener("input", debounce(e => search(e.target.value), 500));
</script>
Live preview

Here is the flow as you type:

  1. debounce wraps your function and keeps a private timer in a closure.
  2. Every keystroke fires input, which calls the wrapped function.
  3. clearTimeout(timer) cancels any run that was scheduled but has not happened yet.
  4. setTimeout(..., 500) schedules the real search to run in 500ms — but only if no new keystroke cancels it first.
  5. So while you keep typing, the action keeps getting pushed back; it finally runs once, 500ms after your last keystroke.

Note: Output: Type "lap" quickly → nothing happens yet. Stop typing for half a second → "Searching for: lap" appears once. (Without debounce, it would have searched three times — once per letter.)

Throttling — a steady maximum rate

Throttling is different: instead of waiting for a pause, it lets the function run immediately, then ignores further calls until a set interval has passed. Picture a turnstile that lets one person through, then locks for two seconds no matter how many push on it. This keeps a steady, predictable rate — ideal for scroll or resize.

Throttle: at most one run per second, however fast you click
<button id="tick">Click me fast!</button>
<p id="count">Allowed runs: 0</p>

<script>
  let runs = 0;
  const out = document.getElementById("count");

  function throttle(fn, interval) {
    let ready = true;                   // can we run right now?
    return function (...args) {
      if (!ready) return;               // still cooling down → ignore
      ready = false;
      fn(...args);                      // run immediately
      setTimeout(() => { ready = true; }, interval);  // unlock after interval
    };
  }

  const handleClick = throttle(() => {
    runs++;
    out.textContent = "Allowed runs: " + runs;
  }, 1000);

  document.getElementById("tick").addEventListener("click", handleClick);
</script>
Live preview

The wrapped function keeps a ready flag in its closure. The first click runs immediately and sets ready to false; any clicks during the next second are simply ignored. After 1000ms a timer flips ready back to true, allowing the next run. So however furiously you click, the counter rises at most once per second — a guaranteed maximum rate.

Note: Output: Click the button 10 times in one second → "Allowed runs: 1". Wait a second and click again → "Allowed runs: 2". (Throttle enforces a steady rate; the extra clicks are dropped.)

Which one do I use?

PatternWhen it runsBest for
DebounceOnce, after the events STOP for a delaySearch-as-you-type, autosave, validating after a pause
ThrottleAt most once per fixed interval, while events continueScroll, resize, drag, rapid button clicks

Tip: In real projects you will often use a tiny library (like lodash’s debounce/throttle) instead of hand-writing these — but interviewers frequently ask you to implement them from scratch, exactly as shown here. Knowing the timer-plus-closure mechanism is what they are testing.

Q. You want to call a search API only after the user stops typing for a moment. Which pattern fits?

Answer: Debouncing waits for a pause in activity and then runs once, which is exactly right for search-as-you-type so you do not fire an API call on every single keystroke. Throttling enforces a steady maximum rate and suits scroll/resize.

✍️ Practice

  1. Add a debounced handler to a text box that logs the value only 400ms after typing stops.
  2. Throttle a scroll handler so it updates a counter at most once every 500ms.

🏠 Homework

  1. Build a search box that debounces the input and, after the pause, fetches matching results from a public API (e.g. jsonplaceholder users filtered by the typed text).
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →