2026-07-04
Throttling and debouncing look similar — both drop events — but they solve different problems. Debounce waits for silence before acting (good for search-as-you-type). Throttle enforces a ceiling: fire at most once per interval, regardless of how many events arrive. If debounce is "wait until the user stops," throttle is "I don't care how excited you are, you get one turn per second."
Use throttle when you need regular progress during a burst, not just a final result. Scroll handlers are the canonical example: if a user scrolls for 5 seconds, debounce fires once at the end and the UI feels frozen. Throttle fires every 100ms and the UI updates smoothly.
Two flavors matter:
Real example — analytics beacon on scroll depth. You want to record how far users scroll, sampled every 250ms. Without throttling, a 3-second scroll fires ~180 events (scroll fires at ~60Hz). Throttled at 250ms: 12 events. That's a 15× reduction in network calls and log volume with no meaningful loss of fidelity.
Rule of thumb for picking the interval:
The subtle bug: naive throttle implementations lose the trailing event. If the user's last scroll happens 50ms before the window closes and you only fire on leading edge, you miss the final position. Always ask: "if events stop mid-window, does the last payload matter?" If yes, use trailing edge.
Throttle vs rate limiter: throttle drops excess events silently and is client-side ergonomic sugar. A rate limiter rejects with an error (429) and is a server-side contract. Don't confuse "the button feels less spammy" with "the server is protected" — you need both.
Where it lives: lodash's _.throttle, RxJS's throttleTime, and every game engine's input handler. If you're writing it from scratch, use a timestamp + setTimeout combo, not just setInterval — you need to handle the trailing case.
