All posts
A cyclist's hands on the handlebars of an e-bike, with a digital speed display mounted on the stem
Photo by TruckRun on Unsplash

Posted 3 min read Updated

Debounce vs Throttle (With a Real Example)

Debounce waits until things stop; throttle limits how often things happen. One demo, two functions, and a subtle trailing-edge detail most implementations miss.

javascriptweb-developmentsoftware-engineeringclean-code
On this page

I used to think this was just a performance thing.

Until I realised it actually changes how your app feels to use.

The Problem

Imagine you have a search input.

Every time a user types, you trigger an API call.

input.addEventListener("input", (e) => { search(e.target.value); });

This works — but it quickly becomes a problem.

Every keystroke triggers a request, which creates unnecessary load. A simple “hello” becomes 5 requests for a single search.

See It in Action

Here’s a simple demo showing what happens:

Throttle vs Debounce — a live demo on CodePen 

At first, I expected throttle to behave like debounce at the end — but it doesn’t. It only fires on a fixed interval unless you explicitly handle the trailing call.

Debounce

Debounce simply waits until a user stops doing something.

function debounce(fn, delay) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }; }

Every time the user types, the timer resets. The function only runs once the user pauses.

Typing quickly now results in a single call instead of multiple — a much cleaner behaviour.

Throttle

Throttle limits how often something can happen.

function throttle(fn, limit) { let lastCall = 0; return function (...args) { const now = Date.now(); if (now - lastCall >= limit) { lastCall = now; fn(...args); } }; }

Instead of waiting, it allows execution at fixed intervals.

Typing now results in steady updates — not spammy, but still responsive.

The Key Difference

The difference is subtle, but important:

A Subtle Detail About Throttle

One thing that caught me off guard is that throttle doesn’t always behave the way you might expect.

A basic implementation (like the one above) only runs on the leading edge — meaning it fires immediately, but won’t run again once the user stops typing.

If you want that final call — similar to debounce — you need to explicitly handle what’s known as a trailing call.

The reason is the macrotask queue that timers run on: a trailing call is just one more setTimeout competing with the work already queued ahead of it, so it only lands if you schedule it deliberately.

In the demo above, try switching between throttle modes — you’ll notice that one version fires immediately, while the other also captures the final input.

When to Use Each

Use debounce when you care about the final result:

Use throttle when you care about continuous updates:

Why This Matters

This isn’t just about reducing API calls.

It’s about controlling how your application reacts over time.

Without this control, your app can feel:

With it, things feel:

Final Takeaway

Small changes like this don’t just improve performance — they change the experience of using your application.

And often, that’s what users notice the most.


Originally published on Medium .

2026 © Jordan O’Hara