How would you handle throttling, debouncing, or optimization to prevent excessive requests
Debounce for 'wait until the user stops' (search-as-you-type); throttle for 'at most once per interval' (scroll, resize). Plus: cancel stale in-flight requests (AbortController), cache/dedupe (React Query), batch, and paginate. Pick the technique by the event's nature.
"Excessive requests" has several causes; the fix depends on which. Know the toolbox and when each applies.
Debounce vs throttle — the core distinction
- Debounce — wait until activity stops for N ms, then fire once. For events where only the final value matters: search-as-you-type, autosave, resize-then-recalculate.
const debouncedSearch = debounce((q) => fetchResults(q), 300);- Throttle — fire at most once per N ms during continuous activity. For events where you want periodic updates: scroll position, mousemove, infinite-scroll triggers, drag.
Mnemonic: debounce = "wait for the pause," throttle = "rate-limit a stream."
The rest of the toolbox
Cancel stale requests. Debouncing reduces how often you fire, but in-flight requests can still resolve out of order. Use AbortController to cancel the previous request when a new one starts — prevents the race where an old response overwrites a newer one.
const controller = new AbortController();
fetch(url, { signal: controller.signal });
// on next input: controller.abort();Cache & dedupe. A library like React Query / SWR caches by key, dedupes identical concurrent requests, and serves cached data instantly — often the biggest win. Don't refetch what you already have.
Batch. Combine many small requests into one (e.g. fetch 50 ids in a single call instead of 50 calls). DataLoader-style batching.
Paginate / virtualize. Don't request 10k rows — request a page at a time, load more on demand.
Guard at the source. Disable the submit button while a request is pending; { leading: true } debounce for immediate first response.
Putting it together — search-as-you-type
Debounce the input (300ms) → on fire, abort the previous request → React Query caches results by query string → minimum length guard (don't search on 1 char). That's debounce + cancellation + cache + guard, layered.
The framing
"First I pick the right tool for the event: debounce when only the final value matters — search input, autosave — throttle when I want periodic updates from a continuous stream — scroll, resize. But debouncing alone isn't enough: I cancel stale in-flight requests with AbortController to kill out-of-order races, cache and dedupe with React Query so I don't refetch known data, and batch or paginate to cut request count structurally. They layer — a real search box uses all of them."
Follow-up questions
- •When do you debounce vs throttle?
- •Why isn't debouncing enough — what does AbortController add?
- •How does React Query reduce request count?
- •What's the difference between leading and trailing debounce?
Common mistakes
- •Using debounce where throttle is needed (or vice versa).
- •Debouncing but not cancelling in-flight requests — out-of-order responses.
- •Re-creating the debounced function every render so it never debounces.
- •Not caching, so the same query refetches repeatedly.
Performance considerations
- •Debounce/throttle cut event-handler and request frequency; cancellation prevents wasted work and race bugs; caching eliminates redundant network entirely. Together they reduce both network load and re-render churn.
Edge cases
- •An older response resolving after a newer one (race condition).
- •Debounced function losing its timer on re-render (wrap in useMemo/useRef).
- •User submits via Enter before the debounce fires.
- •Component unmounts with a pending debounced call — cancel on cleanup.
Real-world examples
- •Search-as-you-type: debounced input + AbortController + React Query cache.
- •Throttled scroll handlers for infinite scroll and sticky headers.