---
title: "Debounce vs Throttle: Wait for Quiet, or Keep a Steady Pace · Ihab Khattab"
description: "Debounce and throttle both stop your code from running on every single event. One waits until things calm down, the other runs at a steady pace. Here's how each works, with demos you can play with."
url: "https://ihabkhattab.com/blog/debounce-vs-throttle/"
---

# Debounce vs Throttle: Wait for Quiet, or Keep a Steady Pace

Oct 16, 2026 · 4 min read

Some events fire a lot. Typing “javascript” fires 10 `input` events. One second of scrolling can fire 60 `scroll` events. You rarely need to react to every one of them: nobody wants 10 API requests for one search.

Debounce and throttle are two ways to say “not every time”. Here is the whole difference in one picture:

_Each thin line is an event, each dot is a run (wait: 500 ms). Debounce runs once, after the events stop. Throttle runs at a steady pace while they keep coming._

## Debounce: Wait Until Things Calm Down

Think of an elevator. When someone walks in, the doors wait a few seconds in case someone else is coming. Each new person restarts the wait. The doors close only when nobody has come in for a while: one trip, however many people got in.

That’s debounce: every new event restarts the timer, and the function runs only after a quiet period.

```js
function debounce(fn, wait) {
  let timer;

  return (...args) => {
    clearTimeout(timer); // Someone walked in: cancel the countdown...
    timer = setTimeout(() => fn(...args), wait); // ...and start a new one.
  };
}
```

_The name comes from electronics. When you press a physical button, its metal contacts bounce for a few milliseconds, so one press looks like several. “Debouncing” means waiting for the signal to settle before counting the press._

Try it. Move your mouse over the box and watch the lanes:

Keep moving and it never runs. That’s not a bug: debounce only cares about the moment you stop.

## Throttle: Keep a Steady Pace

Think of a kid in the back seat asking “Are we there yet?” every five seconds. A tired parent answers the first time, then at most once a minute, no matter how often the kid asks. If the kid asked during that minute, the parent answers once more when it’s up, so the last question isn’t ignored.

That’s throttle: run right away, ignore calls during a cooldown, then run once more with the latest call if one came in.

```js
function throttle(fn, wait) {
  let lastRun = 0;
  let timer;

  return (...args) => {
    clearTimeout(timer);
    const remaining = wait - (Date.now() - lastRun);

    if (remaining <= 0) {
      // Cooldown is over: run now.
      lastRun = Date.now();
      fn(...args);
    } else {
      // Still cooling down: run the latest call when the cooldown ends.
      timer = setTimeout(() => {
        lastRun = Date.now();
        fn(...args);
      }, remaining);
    }
  };
}
```

_The name comes from cars: the throttle controls how much fuel reaches the engine. Think of a speed limiter: press the pedal as hard as you want, the car won’t go faster than its limit._

Simpler versions online often skip the `else` part and lose the last call. If someone scrolls to the bottom during a cooldown, you never hear about it.

## Side by Side

Move in short bursts, then in one long move. Try a longer wait with the slider.

If you remember one thing: **do you need the final value, or updates along the way?**

| Final value: **debounce** | Updates along the way: **throttle** |
| --- | --- |
| Search once they stop typing | Scroll position while they scroll |
| Autosave once they stop editing | Infinite scroll: are we near the bottom yet? |
| “Is this username taken?” once they pause | Drag and drop, live cursors |
| Layout once they stop resizing the window | Progress updates and analytics while they read |

## Real Example: A Search Box

```js
const search = debounce(async (query) => {
  const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
  showResults(await response.json());
}, 300);

input.addEventListener("input", (event) => search(event.target.value));
```

Type a word at your normal speed:

Between 200 and 500 ms usually feels right. Shorter, and you still search mid-word. Longer, and the search feels slow.

Debounce doesn’t fix one thing: responses can arrive out of order. If “ber” is slow and “berlin” is fast, the “ber” results can land last and overwrite the right ones. Cancel the old request when you start a new one:

```js
let controller;

const search = debounce(async (query) => {
  controller?.abort(); // Cancel the request that is still running, if any.
  controller = new AbortController();

  try {
    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal });
    showResults(await response.json());
  } catch (error) {
    if (error.name !== "AbortError") throw error;
  }
}, 300);
```

## Real Example: Scroll Tracking for Analytics

To learn how far people read, you send the scroll depth to your analytics tool. That’s a job for throttle: you want updates while they scroll. With debounce, a slow, steady reader would send nothing until the very end.

```js
const trackScrollDepth = throttle(() => {
  const scrolled = window.scrollY + window.innerHeight;
  const percent = Math.round((scrolled / document.documentElement.scrollHeight) * 100);
  analytics.track("scroll_depth", { percent });
}, 1000);

window.addEventListener("scroll", trackScrollDepth, { passive: true });
```

Tracking search terms is the opposite case. You want “berlin”, not “b”, “be”, “ber”, and so on, so use debounce with a longer wait:

```js
const trackSearch = debounce((query) => analytics.track("search", { query }), 1000);
```

One catch: if someone closes the tab during the wait, the event is lost. For events you can’t lose, send what’s pending on `pagehide`. Lodash’s debounce has a `flush()` method for this.

## A Mistake That’s Easy to Make

The returned function remembers its timer between calls. Create a new one every time and nothing is debounced:

```js
// ❌ A new debounced function on every event. Every call goes through.
input.addEventListener("input", (event) => debounce(search, 300)(event.target.value));

// ✅ Create it once, call it many times.
const debouncedSearch = debounce(search, 300);
input.addEventListener("input", (event) => debouncedSearch(event.target.value));
```

In React, the same happens if you call `debounce()` in a component body: every render makes a new one. Create it once with `useMemo` or `useRef`.

## Should You Write Your Own?

The versions here are fine for most cases. [Lodash](https://lodash.com/docs/#debounce) adds `cancel()`, `flush()`, and options like `leading` and `maxWait`. Fun fact: lodash’s `throttle` is just `debounce` with `maxWait` set to the wait. A debounce that refuses to wait forever is a throttle.

## Node.js Now Has Them Built In

Since [Node.js 24.22.0](https://nodejs.org/en/blog/release/v24.22.0) and 26.10.0, `node:util` has both. Only one of them works like the versions in this post.

**`util.debounce` is the same idea**, with `leading`, `cancel()`, and `flush()` built in. It returns a promise, and all calls in the same window get the same result. A file watcher is a good fit, since saving one file can fire several change events:

```js
import { watch } from "node:fs";
import { debounce } from "node:util";

const rebuild = debounce(() => console.log("Rebuilding..."), 200);
watch("./src", { recursive: true }, rebuild);
```

**`util.throttle` is a different idea.** It’s a rate limiter: at most N calls start per interval, and the rest wait in line. Nothing is dropped:

_Five quick calls. Our throttle (500 ms) runs the first and the latest, and drops the rest. util.throttle(fn, 2, 500) runs all of them, two per half second, in order._

That’s perfect for staying under an API’s “2 requests per second” limit:

```js
import { throttle } from "node:util";

const getUser = throttle(async (id) => {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
}, 2, 1000);

const users = await Promise.all(userIds.map((id) => getUser(id)));
```

But it’s the wrong tool for scroll or mouse events: every event would be queued and run, just later. `{ overflow: "drop" }` rejects the extra calls instead, but then the last call, the one with the final position, is dropped too.

## To Sum Up

- **Debounce** is the elevator door: it waits until people stop coming in, then goes once.
- **Throttle** is the tired parent: it answers at most once a minute, however often you ask.
- Need the **final value**? Debounce. Need **updates along the way**? Throttle.
- Create the debounced or throttled function once, and reuse it.
- In Node.js, `util.debounce` is a regular debounce, but `util.throttle` is a rate limiter that queues calls.

[← All posts](https://ihabkhattab.com/blog/)
