p-throttle: rate limiting that queues calls instead of dropping them
Throttle promise-returning & async functions
At a glance
- What is it?
- p-throttle is a small JavaScript library that limits how often an async function runs without discarding any call, so every request eventually executes with its own arguments and context intact. It suits anyone calling a rate limited external API, and it needs Node 20 or later plus the browser support for WeakRef and FinalizationRegistry.
- Who is it for?
- Adopt p-throttle when you are calling an external API that enforces a request rate and you cannot afford to lose calls, because it queues everything and guarantees the last call runs with its original arguments and context, and it adds weighted limits for APIs whose quota is cost-based rather than count-based.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 35 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The distinction that makes p-throttle different
p-throttle's whole point is stated in one sentence: it rate-limits function calls without discarding them, making it suitable for external API interactions where losing a call is not acceptable. All calls are queued and executed, and the last call is guaranteed to run with its original context and arguments preserved.
That is the difference from the throttle implementations most people meet in a browser. The familiar `throttle` from lodash or the CSS notion of throttling is a filter: the calls that arrive too fast are dropped, and the caller has to accept the loss. For a UI event handler that is correct behaviour, because the last value is all that matters. For a write to a remote API it is not, because the dropped call is work that was intended and never happened.
So the model here is a queue with a gate rather than a filter. You get a function back, you call it, and it returns a promise that resolves when that particular call has actually run. The library decides when, based on a limit and an interval you give it, and it never drops anything on the floor.
The library also works with normal functions, not only promise-returning ones, and the package is MIT licensed, authored by Sindre Sorhus, and published as an ES module with type declarations. Installation is a single npm command:
npm install p-throttleThe declared Node requirement is `>=20`, and the browser support note says it works in modern browsers supporting `WeakRef` and `FinalizationRegistry`, which the README puts as Chrome 84+, Firefox 79+, Safari 14.1+ and Edge 84+.
The two options you must set, and the two algorithms behind them
The `pThrottle(options)` function returns a throttle function, and both `limit` and `interval` must be specified.
`limit` is the maximum number of calls within an `interval`, and `interval` is the timespan for `limit` in milliseconds. There is no default for either, which is a deliberate design choice: a throttler with a silent default would be a throttler that surprises you.
The `strict` option is the one worth reading twice. It is a boolean defaulting to `false`, and it selects between two algorithms. The default uses a windowed approach that works correctly in most cases, limiting the total number of calls at the specified limit per interval window. The strict algorithm throttles each call individually, ensuring the limit is not exceeded for any interval, and it is described as a strict, more resource-intensive throttling algorithm.
The distinction matters for a specific case. A windowed limiter can let a burst through at a window boundary. If your API quota is expressed as a rolling window, or the endpoint counts bursts rather than an average rate, the default behaviour can exceed the limit you asked for. The strict algorithm pays for that guarantee in per-call bookkeeping.
The README's opening example shows the default behaviour with `limit: 2` and `interval: 1000`, which calls the function at most twice per second. The throttled function is created from an async arrow, six calls are fired in a loop, and the printed output shows the six results arriving at 0s, 0s, 1s, 1s, 2s and 2s.
So the first two calls run immediately and the rest queue behind them, spaced across the interval. Nothing is lost, and the timing is what you would want from a rate limited endpoint.
Weight: rate limits that cost points rather than counting calls
The `weight` option is the one that sets this library apart from a plain limiter, and it addresses a real class of API.
`weight` is a function that calculates the weight or cost of each function call based on its arguments. The weight determines how much of the `limit` is consumed by each call. The README says this is useful for rate limiting APIs that use point-based or cost-based limits, where different operations consume different amounts of the quota. By default each call has a weight of `1`.
The example is a GraphQL API budgeted at 100 points per second, where each query costs 1 point for the connection plus 1 point per table. The weight function is `numberOfTables => 1 + numberOfTables`, so a query touching one table costs 2 points and a query touching three tables costs 4. The limit stays 100 with a 1000 millisecond interval, and the two calls in the example are annotated as costing 2 and 4 points.
This is a different thing from counting calls. A naive limiter that allows 100 calls per second against a point-based API can exceed the quota badly, because a single expensive query might cost more than a hundred cheap ones. Counting calls is only correct when every call costs the same.
The `onDelay` option is the other half of the observability story. It is a function called when function calls are delayed due to exceeding the `limit` of allowed calls within the given `interval`, and the delayed call's arguments are passed to the callback. The README suggests it can be useful for monitoring the throttling efficiency, and the example shows a third call being delayed after two immediate ones, with the callback printing the delay for arguments 5 and 6.
Abort, queue size, and turning the throttle off
Three smaller features matter for production use, and each of them addresses a failure mode.
The `signal` option takes an `AbortSignal` and aborts pending executions. When aborted, all unresolved promises are rejected with `signal.reason`. In the README's example, an `AbortController` is created and its signal passed into the throttle, two calls run, then `controller.abort('aborted')` is called and the next call rejects with the reason `aborted`. The practical value is in shutdown: a process that is stopping can reject the queued calls rather than leaving them pending until the process is killed, and the rejection carries a reason the caller can inspect.
`throttledFn.queueSize` is a number giving the number of queued items waiting to be executed. The README says this can be useful for implementing queue management strategies, such as using a fallback when the queue is too full, and the example checks `accurateData.queueSize >= 3` and switches to a rougher endpoint when the accurate one is backed up. That is a real pattern for a client that has a degraded mode.
`throttledFn.isEnabled` is a boolean defaulting to `true`, and it controls whether future function calls should be throttled and count towards throttling thresholds. Setting it to false lets you bypass the limit for a specific stretch, which is what you want for a bulk operation you have permission to run quickly, or for turning the limit off while debugging.
Together those three give you the levers you would otherwise write by hand around a throttler: how many are waiting, how to cancel them, and how to step outside the limit deliberately.
Where p-throttle is the wrong tool
The first wrong-tool case is concurrency control rather than rate control. If what you want is to run at most five tasks at once regardless of timing, that is a concurrency limiter, and the README points at p-limit, which runs multiple promise-returning and async functions with limited concurrency. The two problems look similar and are solved differently: a throttle limits how often a call starts, a limit controls how many are in flight.
The second is a burst of events where only the last one matters. If a user types in a search box and you want one request per pause in typing, a debounce is the right shape, and the README points at p-debounce for the promise-returning version. p-throttle will happily run every intermediate value, just slowly, and if you did not need them that is wasted work on both ends.
The third is a browser bundle where the platform is too old. The library relies on `WeakRef` and `FinalizationRegistry`, so the documented support is Chrome 84+, Firefox 79+, Safari 14.1+ and Edge 84+. Below those, the package simply does not work, and there is no documented polyfill path.
The fourth is a Node version below 20. The package declares `engines.node` as `>=20`, and it is an ES module with an `exports` map that only exposes the default entry, so a CommonJS consumer needs a dynamic import rather than a `require`.
The alternative worth naming is writing the queue yourself, which for one function is about twenty lines with a timer and an array. The difference in approach is that p-throttle gives you the two things that are easy to get subtly wrong: it guarantees the last call runs with its original context and arguments, and it exposes queue size and an abort path so you can manage the backlog rather than discovering it. A hand-rolled version tends to grow those features one outage at a time.
Maintenance, packaging and the test setup
The package is actively worked on. Version 8.1.1 was released on 2026-09-12, 8.1.0 on 2025-11-08 and 8.0.0 on 2025-08-21, and the last push to the default branch was on 2026-09-18.
The packaging is modern and deliberately narrow. The `exports` map has just a `types` entry pointing at `index.d.ts` and a `default` entry pointing at `index.js`, and `sideEffects` is `false`, which lets a bundler drop the module entirely if nothing imports it. The `files` array contains only `index.js` and `index.d.ts`, so the published tarball is the implementation and its types.
The test setup is worth noting because it explains the browser support requirement. The `test` script runs `xo` for linting, `ava` for the unit tests and `tsd` for the type tests, and there is a separate `test-gc` script that runs `node --expose-gc gc-test`, a separate file at the top level named `gc-test.js`. The Ava configuration sets `serial: true`, meaning the test run is serialised rather than parallel, which is the usual choice for a library whose tests are timing sensitive.
That `gc-test.js` file is the practical explanation for `FinalizationRegistry`. The library has to clean up state for throttled functions that are garbage collected, and testing that behaviour needs the garbage collector to be exposed and triggered deterministically, which is what `--expose-gc` is for. It is a small detail, but it is the kind of detail that explains why a small library carries a browser support floor.
The licence is MIT, with a `license` file at the top level. The repository also carries `index.test-d.ts` for the type tests and a `tests/` directory, and the README links to the sibling packages in the same family, p-debounce, p-limit, p-memoize and a broader promise function collection.
Editorial conclusion
Adopt p-throttle when you are calling an external API that enforces a request rate and you cannot afford to lose calls, because it queues everything and guarantees the last call runs with its original arguments and context, and it adds weighted limits for APIs whose quota is cost-based rather than count-based. Do not adopt it as a concurrency limiter, since that is p-limit's job, and do not adopt it for browser code without checking that the target supports WeakRef and FinalizationRegistry. Before you ship it, decide whether you want the windowed default algorithm or the strict per-call one, since the strict option is documented as more resource-intensive, and wire up the `signal` option so a shutdown can reject the pending queue rather than leaving it hanging.
Frequently asked questions
What does p-throttle do with calls that exceed the limit?
It queues them rather than discarding them. The README states that all calls are queued and executed, and the last call is guaranteed to run with its original context and arguments preserved, which is what makes it suitable for external API interactions where losing a call is not acceptable.
How do I install p-throttle and what does it require?
Install it with `npm install p-throttle`. The package requires Node 20 or later, and in the browser it needs `WeakRef` and `FinalizationRegistry`, which the README puts as Chrome 84+, Firefox 79+, Safari 14.1+ and Edge 84+.
What is the difference between the default and the strict throttling algorithm?
The default uses a windowed approach that works correctly in most cases, limiting the total number of calls at the specified limit per interval window. The `strict` option enables a more resource-intensive algorithm that throttles each call individually, ensuring the limit is not exceeded for any interval.
What is the weight option for in p-throttle?
The `weight` function calculates the cost of each call from its arguments, and that cost is what consumes the `limit`. It is for APIs with point-based or cost-based quotas, where different operations consume different amounts, and each call has a weight of 1 by default.
How do I stop queued calls in p-throttle?
Pass an `AbortSignal` in the `signal` option. When the signal is aborted, all unresolved promises are rejected with `signal.reason`, so a shutting-down process can clear the queue instead of leaving calls pending. The `throttledFn.queueSize` property also reports how many calls are waiting, and `throttledFn.isEnabled` can switch throttling off for future calls.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sindresorhus-p-throttle)Was this useful?