mrkeyoor.com_
Sat 08 Aug 21:01 UTC
npmUtilsupdated 08 Aug 2026

p-throttle

p-throttle wraps a function so calls start no faster than a configured quota, such as five calls per second. Excess calls wait in an in-memory queue and keep their original arguments and receiver; they are not dropped. Version 8 also supports strict rolling-window enforcement, weighted calls, aborting pending work, delay observation, queue-size inspection, and temporarily bypassing the limiter. It limits start rate, not simultaneous work, retries, distributed traffic, or server-side user quotas.

Verdict

An excellent small wrapper for pacing one process against an upstream quota, and version 8's weights and queue visibility cover the common cases. Do not mistake it for a concurrency pool or distributed rate limiter, and put your own admission policy in front of its unbounded queue.

API stability4/5The factory-then-wrapper shape and limit plus interval options have remained straightforward, while newer releases added strict mode, signals, queueSize, isEnabled, and weights without complicating ordinary calls. Major versions follow the author's modern-runtime policy, however: 8.1.0 requires Node 20 and ESM, so CommonJS consumers and older Node deployments face a deliberate compatibility break.
Docs5/5The README documents every option with runnable examples, distinguishes the default window algorithm from strict enforcement, states browser feature requirements, and explains that calls queue rather than disappear. queueSize, signal rejection, weighted costs, and isEnabled are all described. The main missing production guidance is that the queue is unbounded and start-rate limiting does not cap concurrent work.
Maintenance4/5Version 8.1.0 was published November 8, 2025 and the repository was pushed at the same time. It is not archived and GitHub reports no open issues or pull requests. There has been no push in the following nine months, but the zero-dependency package has a small surface, current Node 20 floor, recent weighted-call feature, tests, and bundled declarations, all consistent with maintained stable software.
Ecosystem4/5The package recorded 4,977,351 downloads in the measured week and has 518 GitHub stars. It belongs to the widely used promise-fun family and composes naturally with p-limit, AbortController, and fetch. It has no plugin system or shared backend, which is intentional, but that keeps it out of distributed rate-limiting and durable queue ecosystems.

Use it if

  • One Node process or browser tab must pace calls to an external API without dropping queued work
  • An API publishes a simple requests-per-interval or points-per-interval quota
  • You need to observe queue pressure, reject pending calls with AbortSignal, or preserve method context
  • You want a dependency-free wrapper with bundled TypeScript types rather than a scheduler framework
Skip it if

Setup reality

Installation has no peers, native build, credentials, or config file, but version 8.1.0 requires Node 20 and publishes ESM only. Use import pThrottle from 'p-throttle'; require() is not a supported entry. Browsers need WeakRef and FinalizationRegistry, with the README naming Chrome 84+, Firefox 79+, Safari 14.1+, and Edge 84+ as baselines. Both limit and interval are mandatory, interval is milliseconds, and the default algorithm uses fixed windows. That can permit a burst near one window boundary followed by another just after it; strict: true enforces the quota over any interval at a higher resource cost. Calls are queued without a built-in maximum. Watch the wrapped function's queueSize and shed, reject, or fall back before memory and latency grow unnoticed. AbortSignal rejects unresolved queued promises with signal.reason, but it cannot undo work that has already started unless your wrapped operation also receives and observes a signal. Weighted calls must return sensible positive costs that fit the quota. Setting isEnabled to false bypasses throttling for future calls and those calls do not count, so it is an operational escape hatch rather than a pause. Finally, rate and concurrency are separate: combine p-throttle with p-limit when slow operations could overlap beyond a safe connection or memory ceiling, and create one shared wrapper per quota instead of accidentally giving each request its own fresh counter.

Patterns

Limit calls per intervalthrottle-api-calls

import pThrottle from 'p-throttle';

const throttle = pThrottle({ limit: 5, interval: 1000 });
const getUser = throttle(async (id) => {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
});

The quota controls when calls start. Slow fetches can overlap, and failed calls still consumed a start slot.

Queue a batch while preserving result orderprocess-batch

const results = await Promise.all(
  userIds.map((id) => getUser(id))
);

All calls are queued immediately and Promise.all keeps input order, but a very large batch can create a very large in-memory queue.

Use strict rolling-window enforcementenforce-rolling-window

const strictThrottle = pThrottle({
  limit: 10,
  interval: 1000,
  strict: true,
});

Strict mode prevents boundary bursts across any interval, but the README calls it more resource-intensive than the default windowed algorithm.

Charge different costs by argumentweight-expensive-calls

const throttle = pThrottle({
  limit: 100,
  interval: 60_000,
  weight: (query) => 1 + query.tables.length,
});

const runQuery = throttle((query) => api.query(query));

weight is evaluated from call arguments and consumes that much quota. Keep weights positive and within the configured limit.

Reject pending work with AbortControllerabort-pending-calls

const controller = new AbortController();
const throttle = pThrottle({
  limit: 2,
  interval: 1000,
  signal: controller.signal,
});
const task = throttle((id) => fetchItem(id));

const pending = [task(1), task(2), task(3)];
controller.abort(new Error('shutdown'));
await Promise.allSettled(pending);

Aborting rejects unresolved queued promises with signal.reason. Work already started needs its own cancellation signal.

Report calls delayed by the quotaobserve-delays

const throttle = pThrottle({
  limit: 20,
  interval: 1000,
  onDelay: (endpoint) => metrics.increment('api.throttled', { endpoint }),
});
const request = throttle((endpoint) => fetch(endpoint));

onDelay receives the wrapped call's arguments. Avoid expensive or throwing monitoring code in this callback.

Reject new work when the queue is too deepshed-overloaded-queue

async function guardedGet(id) {
  if (getUser.queueSize >= 100) {
    throw new Error('upstream queue overloaded');
  }
  return getUser(id);
}

queueSize is observational and can change between the check and call. It is still useful for coarse admission control in one event loop.

Fall back when accurate data is backed upuse-fallback-on-pressure

async function getPrice(sku) {
  if (accuratePrice.queueSize >= 10) {
    return cachedPrice(sku);
  }
  return accuratePrice(sku);
}

The fallback avoids adding more queued calls; p-throttle itself never discards or replaces pending work.

Temporarily bypass the limiterbypass-throttling

throttledHealthCheck.isEnabled = false;
try {
  await throttledHealthCheck();
} finally {
  throttledHealthCheck.isEnabled = true;
}

Disabled future calls execute without waiting and do not count toward thresholds. This does not pause or drain the existing queue.

Throttle an object method with its receiverpreserve-method-context

const client = {
  token: 'secret',
  async load(path) { return fetch(path, { headers: { Authorization: this.token } }); },
};
client.load = throttle(client.load);
await client.load('/account');

The wrapper preserves the call's original this value and arguments. Calling an extracted method without bind or a receiver still loses context.

Share one throttle across operationsshare-one-quota

const throttleVendor = pThrottle({ limit: 50, interval: 1000 });
export const fetchUser = throttleVendor((id) => vendor.get(`/users/${id}`));
export const fetchOrder = throttleVendor((id) => vendor.get(`/orders/${id}`));

Functions wrapped by the same throttle factory share its quota. Creating a new factory per operation would create independent counters.

Limit starts and simultaneous workcombine-rate-and-concurrency

import pLimit from 'p-limit';

const concurrency = pLimit(4);
const startRate = pThrottle({ limit: 10, interval: 1000 });
const scheduledFetch = startRate((url) => concurrency(() => fetch(url)));

p-throttle limits starts per interval; p-limit caps unresolved operations. The two controls solve different overload modes.

Alternatives

PackageRegistryPick it when
bottlenecknpmYou need priorities, reservoirs, clustering, retries, and a richer job scheduler
p-limitnpmYou need to cap concurrent promise executions rather than starts per time interval
rate-limiter-flexiblenpmYou enforce distributed or per-user quotas using Redis, databases, or other shared stores
limiternpmYou want token-bucket and interval rate limiters with explicit token removal