{"post":{"slug":"how-to-throttle-requests-2026","title":"How to throttle requests in 2026","excerpt":"Throttling limits how often a function or HTTP request fires, protecting APIs and keeping UIs smooth. This covers client-side throttle, rate limiting, and AbortController.","category":"code","date":"April 4, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">T</span>hrottling is the mechanism that keeps a high-frequency event from triggering an action on every occurrence. A scroll handler without throttle fires hundreds of times per second. An API client without throttle hits rate limits and gets blocked. The pattern is simple; knowing when to use it over debounce is the skill.\n\n## What changed in 2026\n\n- **AbortController is universally supported** and is the standard way to cancel superseded fetch requests — pair it with debounce or throttle for search inputs.\n- **The Fetch API accepts signals natively** — no library wrapper needed.\n- **Edge rate limiters** (Cloudflare, Vercel) handle server-side throttle transparently; you still need client-side patterns for UX smoothness.\n- **`use-debounce` and `throttle-debounce` packages** are the standard React ecosystem choices — both tiny and typed.\n\n## Plain JavaScript throttle\n\n```javascript\nfunction throttle(fn, interval) {\n  let lastCall = 0;\n  return function (...args) {\n    const now = Date.now();\n    if (now - lastCall >= interval) {\n      lastCall = now;\n      return fn.apply(this, args);\n    }\n  };\n}\n\n// Fires at most once every 200ms while scrolling\nconst onScroll = throttle(() => {\n  updateScrollIndicator(window.scrollY);\n}, 200);\n\nwindow.addEventListener(\"scroll\", onScroll);\n```\n\n## TypeScript version\n\n```typescript\nfunction throttle<T extends (...args: unknown[]) => void>(\n  fn: T,\n  interval: number\n): (...args: Parameters<T>) => void {\n  let lastCall = 0;\n  return (...args: Parameters<T>) => {\n    const now = Date.now();\n    if (now - lastCall >= interval) {\n      lastCall = now;\n      fn(...args);\n    }\n  };\n}\n```\n\n## React scroll handler with useRef\n\n```tsx\nimport { useRef, useEffect } from \"react\";\n\nfunction ScrollTracker() {\n  const lastCallRef = useRef(0);\n\n  useEffect(() => {\n    const handleScroll = () => {\n      const now = Date.now();\n      if (now - lastCallRef.current >= 200) {\n        lastCallRef.current = now;\n        console.log(\"scroll Y:\", window.scrollY);\n      }\n    };\n\n    window.addEventListener(\"scroll\", handleScroll, { passive: true });\n    return () => window.removeEventListener(\"scroll\", handleScroll);\n  }, []);\n\n  return null;\n}\n```\n\n`{ passive: true }` tells the browser the handler will not call `preventDefault` — it can optimize scroll painting accordingly.\n\n## Throttling fetch calls with AbortController\n\nWhen you need the most recent result and want to cancel stale requests:\n\n```typescript\nlet abortController: AbortController | null = null;\n\nasync function searchWithCancel(query: string): Promise<void> {\n  // Cancel any in-flight request\n  abortController?.abort();\n  abortController = new AbortController();\n\n  try {\n    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {\n      signal: abortController.signal,\n    });\n    const data = await res.json();\n    renderResults(data);\n  } catch (err) {\n    if (err instanceof Error && err.name === \"AbortError\") {\n      // Expected — previous request superseded\n      return;\n    }\n    throw err;\n  }\n}\n```\n\nCombine with debounce so the API is only called after 300 ms of silence, and AbortController cleans up any overlap.\n\n## Handling 429 Too Many Requests\n\n```typescript\nasync function fetchWithRetry(\n  url: string,\n  maxRetries = 3\n): Promise<Response> {\n  for (let attempt = 0; attempt < maxRetries; attempt++) {\n    const res = await fetch(url);\n    if (res.status === 429) {\n      const retryAfter = res.headers.get(\"Retry-After\");\n      const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 1000 * 2 ** attempt;\n      await new Promise((r) => setTimeout(r, waitMs));\n      continue;\n    }\n    return res;\n  }\n  throw new Error(`Failed after ${maxRetries} retries`);\n}\n```\n\nAlways read `Retry-After` before deciding how long to wait — the server is telling you the exact back-off period.\n\n## Throttle vs debounce — which to use\n\n| Scenario | Pattern | Why |\n| --- | --- | --- |\n| Scroll position tracking | Throttle (100–200 ms) | Want periodic updates while scrolling |\n| Search / autocomplete input | Debounce (250–350 ms) | Want the final value, not intermediate ones |\n| Window resize handler | Debounce (100–200 ms) | Care about the end size, not every pixel |\n| Analytics event (mousemove) | Throttle (500 ms) | Sample regularly, not every pixel |\n| Auto-save form | Debounce (800–1 500 ms) | Save after typing stops |\n| Button spam guard | Leading debounce | Fire once immediately, ignore follow-ups |\n\n## Server-side rate limiting basics (Node.js)\n\n```typescript\nimport rateLimit from \"express-rate-limit\";\n\nconst limiter = rateLimit({\n  windowMs: 60 * 1000,  // 1 minute\n  max: 60,              // 60 requests per minute per IP\n  standardHeaders: true, // include Retry-After header\n  legacyHeaders: false,\n});\n\napp.use(\"/api/\", limiter);\n```\n\n`express-rate-limit` + `rate-limit-redis` backs the store in Redis, making it work across multiple Node processes. Without a shared store, each instance has its own counter and limits are per-process, not global.\n\n## Common mistakes\n\n**Throttling in the render body.** Like debounce, creating a throttled function inside a React component recreates it on every render. Use `useRef` or a hook.\n\n**Using throttle for search inputs.** Throttle fires periodically while typing, sending half-finished queries. Debounce is correct — fire only after the user stops.\n\n**No server-side enforcement.** Client-side throttle is a UX optimization. A determined caller bypasses it instantly. Always enforce rate limits on the server.\n\n**Ignoring `Retry-After`.** Retrying a 429 immediately triggers another 429. Read the header.\n\n## What to skip\n\n- **Throttle for form submission** — use a leading debounce (fires once immediately) or a loading state to disable the button.\n- **Implementing throttle with `setInterval`** — polling is not the same as throttling; the semantics are different.\n- **Giant libraries for one utility** — `throttle-debounce` is 2 kB and covers both patterns without the lodash weight.\n\n## FAQ\n\n**Does throttle guarantee exactly one call per interval?**\nNo. The simple implementation fires the first call in the interval and drops the rest. A \"trailing\" throttle also fires the last call after the interval; some libraries offer both modes.\n\n**How do I throttle GraphQL subscriptions?**\nThrottle the state update that triggers a re-render, not the subscription itself. The subscription receives all events; your handler updates state at most once per N ms.\n\n**What is the difference between throttle and a rate limiter?**\nThrottle is a time-based gate on a single caller. A rate limiter tracks calls per unit time across multiple callers (usually identified by IP or API key) and enforces a ceiling.\n\n**Can I use the Web API `requestAnimationFrame` instead of throttle for scroll?**\nYes — `rAF` fires at the display refresh rate (~60–120 Hz), which is roughly 8–16 ms. For scroll animations this is better than a fixed interval; for analytics sampling a fixed throttle is more predictable.\n\n## Where to go next\n\n- [How to debounce a function in 2026](/blog/how-to-debounce-a-function-2026)\n- [How to handle API errors in 2026](/blog/how-to-handle-api-errors-2026)\n- [How to mock an API in 2026](/blog/how-to-mock-an-api-2026)","hue":319,"metaTitle":"How to Throttle Requests in 2026: JS, React, Node.js","metaDescription":"Learn how to throttle function calls and HTTP requests in 2026. Covers JS throttle, AbortController, React patterns, and server-side rate limiting basics.","keywords":["how to throttle requests 2026","javascript throttle function","throttle vs debounce 2026","abortcontroller fetch javascript","react throttle hook","rate limiting nodejs 2026","throttle api calls javascript","throttle scroll handler","http 429 too many requests","client side rate limiting 2026"],"tldr":["**Throttle fires at most once per N ms** — the right tool for scroll handlers, analytics events, and high-frequency UI callbacks.","**Server-side rate limiting is separate** — client throttle is a courtesy; server-side rate limiting is the enforcement layer.","**AbortController cancels in-flight HTTP requests** — combine it with debounce/throttle to avoid stale responses overwriting fresh ones.","**Retry-After headers tell you when to back off** — read them before hammering a 429 with blind retries.","**Skip:** implementing server-side rate limiting with only client-side throttle — a malicious client ignores your JavaScript."],"readTime":"5 min read","wordCount":1049},"related":[{"slug":"what-is-debouncing-vs-throttling-2026","title":"Debounce vs Throttle in 2026: Which One to Use","excerpt":"Debounce and throttle both limit how often a function runs, but they do it differently and fit different events. Here is how to tell them apart in 2026 and pick the right one.","category":"code","date":"July 22, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":310},{"slug":"how-to-debounce-a-function-2026","title":"How to debounce a function in 2026","excerpt":"Debouncing delays a function call until a burst of rapid triggers settles, preventing runaway API calls and laggy UIs. Here is the pattern and when to use it.","category":"code","date":"March 30, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":318},{"slug":"api-rate-limiting-2026","title":"API rate limiting in 2026: algorithms, patterns, and implementation","excerpt":"Rate limiting protects your API from abuse and keeps costs predictable, but the wrong algorithm causes thundering-herd failures. Here is the 2026 guide.","category":"code","date":"March 24, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":306}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-teach-teens-to-invest-2026","title":"How to teach teens to invest in 2026: accounts, lessons, and first trades","excerpt":"Teaching a teenager to invest is the highest-ROI financial lesson you can give them. Here is the 2026 guide to custodial accounts, first investments, and making it stick.","category":"finance","date":"April 4, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":282},"older":{"slug":"how-to-tie-a-bow-tie-2026","title":"How to tie a bow tie in 2026: the real knot, not a clip-on","excerpt":"Tying a real bow tie takes three minutes to learn and looks far better than a pre-tied version. Here is the step-by-step method with the key trick most guides miss.","category":"guides","date":"April 4, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":295}}