mrkeyoor.com_
Sat 19 Sept 21:39 UTC
npmUtilsupdated 19 Sept 2026

p-limit review

p-limit 7.3.2 feeds promise-returning functions through one memory-backed queue and starts only the number allowed by its concurrency setting. Its callable limiter reports running and waiting counts, maps synchronous iterables, discards queued calls, and accepts a new concurrency value while work is pending. `limitFunction()` gives one reusable operation its own cap. The 7.3.2 patch exposes `clearQueue()` on that wrapper, fixing an API gap in 7.3.1. Our 7.3.1 sandbox loaded the package through ESM `import` and `require()`, and the TypeScript declarations were already included. p-limit has no interval clock, retry policy, persistent storage, or coordination between JavaScript processes.

301.6Mdownloads / wk
Verdict

Our p-limit 7.3.1 install completed in 0.3 seconds, left 2 packages in 1 MB, produced a 0.9 KB gzipped browser build, and returned 0 known vulnerabilities from npm audit. Install 7.3.2 for a concurrency ceiling inside one process, and select a different scheduler for clock-based quotas or work shared across processes.

We installed it

Lab card: what happened when we installed p-limitScreenshot of p-limit documentation
Install✓ · 0.3s2 packages on disk · 1 MB
ImportESM import works · require() works · ESM package with exports map
Browser0.9 KBgzipped (1.7 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does p-limit install cleanly?

Yes. In a fresh container with an empty cache, npm install p-limit finished in 0.3s, leaving 2 packages and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does p-limit add to a browser bundle?

0.9 KB gzipped (1.7 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does p-limit work with both ESM and CommonJS?

Yes. Both import 'p-limit' and require('p-limit') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does p-limit include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

p-limit or p-queue: which should you use?

p-queue: Use p-queue when the queue also needs priorities, pausing, events, interval caps, or a timeout for each operation. Our p-limit 7.3.1 install completed in 0.3 seconds, left 2 packages in 1 MB, produced a 0.9 KB gzipped browser build, and returned 0 known vulnerabilities from npm audit.

When should you not use p-limit?

Production is pinned to Node 18 or earlier. The 7.3.2 package metadata requires Node 20 or newer

API stability4/5p-limit 7.3.2 retains the original `pLimit(number)` entry point and the callable `limit(() => work())` shape. Releases in the 7.3 line added an options object, `rejectOnClear`, and `limitFunction.clearQueue()` without forcing ordinary callers to migrate. Major version 7 did raise the runtime floor to Node 20 and changed when `activeCount` increments, so code that treats counters as scheduling signals or still runs Node 18 has a real upgrade boundary.
Docs4/5The 7.3.2 README defines the constructor, the callable limiter, its 5 attached controls, and `limitFunction()` in one page. It states that nested use can deadlock, explains why cleared promises may never settle, and says active calls survive `clearQueue()`. A separate recipes file covers shutdown, partial errors, progress, and changing concurrency. The weak spot is deployment scope: readers must infer from the implementation that every process owns a separate memory queue.
Maintenance5/5Release 7.3.2 was published on August 31, 2026, and the repository's last push landed that same day. GitHub reported 0 open issues and pull requests, with the repository active and unarchived. The release fixes a reproduced `TypeError` by wiring `clearQueue()` onto `limitFunction()` output and updating its declaration, tests, and README. Our separate 7.3.1 install also produced 0 findings from npm audit.
Ecosystem5/5npm counted 239,607,312 p-limit downloads from September 5 through September 11, 2026, while GitHub reported 2,925 stars. The README links directly to p-queue, p-throttle, and p-map for adjacent scheduling jobs. Our 7.3.1 install needed 1 direct dependency, included TypeScript declarations, and generated a 0.9 KB gzipped browser build. Each process still owns a separate queue, so adoption does not create a host-wide limit.

Use it if

  • A Node service must stop a burst of fetches, file reads, or database calls from running all at once
  • Several callers should share one in-process allowance and expose its running and waiting counts to your logs
  • Runtime feedback needs to raise or lower concurrency without rebuilding the queue or changing each call site
  • One repeated async operation should carry its own cap through `limitFunction()`, including queue clearing in 7.3.2
Skip it if

Setup reality

We installed p-limit 7.3.1 in a fresh unprivileged Node 22 Bookworm container with 3 CPUs and 8 GB of RAM. npm finished in 0.3 seconds, and the install left 2 packages occupying 1 MB. The package was 32 KB unpacked, declared 1 direct dependency and 0 peer dependencies, and carried the MIT license. npm audit found 0 known vulnerabilities. Bundled TypeScript declarations loaded without an extra types package. ESM import and require() both succeeded despite the package declaring ESM and an exports map.

p-limit asks for no account, secret, background service, or config file. In 7.3.2 you can pass a number to pLimit() or an options object containing concurrency and rejectOnClear. The queue exists only in the JavaScript process that created it. When a server starts several workers, each worker receives a separate allowance unless another system coordinates them. There is no cache or saved queue to restore after a restart.

Give the limiter a function so it controls when the operation begins: limit(() => fetch(url)). A fetch promise made before that call is already running. limit.map() accepts a synchronous iterable, turns it into an array, and waits with Promise.all. That makes the helper suitable for finite batches. An async stream or an endless producer needs a consumer with backpressure. One rejected task leaves the remaining queued functions eligible to start.

A full limiter can deadlock when every running callback awaits inner work submitted to that same queue. Split the limits by resource or remove the nested scheduling step. clearQueue() cannot stop active operations. With rejectOnClear left at its false default, discarded calls stay unsettled. Setting it to true rejects them with AbortError. The 7.3.2 release makes this method available on limitFunction() wrappers. Running fetches still require their own AbortSignal. Our full browser import measured 1.7 KB minified and 0.9 KB gzipped.

Patterns

Keep four HTTP calls in flight cap-api-fetches

import pLimit from 'p-limit';

const limit = pLimit(4);
const payloads = await Promise.all(
  urls.map((url) => limit(async () => {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  }))
);

The limiter controls each start because `fetch()` is created inside its callback. A fetch promise created earlier has already begun.

Read files through five slots bound-file-reads

import {readFile} from 'node:fs/promises';

const limit = pLimit(5);
const documents = await limit.map(paths, async (path, index) => ({
  index,
  text: await readFile(path, 'utf8'),
}));

`limit.map()` preserves input order through `Promise.all` and converts the synchronous iterable to an array before the batch finishes.

Put one cap around a reusable client call wrap-api-client

import {limitFunction} from 'p-limit';

const loadAccount = limitFunction(
  (id) => api.accounts.get(id),
  {concurrency: 3}
);

const accounts = await Promise.all(ids.map(loadAccount));

Calls made through this `loadAccount` wrapper share one 3-operation allowance in the current process.

Reject calls waiting behind a wrapper clear-wrapped-calls

const sendEmail = limitFunction(deliverEmail, {
  concurrency: 1,
  rejectOnClear: true,
});

const deliveries = messages.map(sendEmail);
sendEmail.clearQueue();
const outcomes = await Promise.allSettled(deliveries);

p-limit 7.3.2 adds `clearQueue()` to a `limitFunction()` result, and waiting calls reject with `AbortError` when `rejectOnClear` is true.

Abort running fetches and clear the wait list stop-active-and-waiting

const controller = new AbortController();
const limit = pLimit({concurrency: 2, rejectOnClear: true});

const requests = urls.map((url) =>
  limit(() => fetch(url, {signal: controller.signal}))
);

controller.abort();
limit.clearQueue();
const outcomes = await Promise.allSettled(requests);

`clearQueue()` rejects only the waiting calls here. The shared `AbortController` sends the cancellation signal to the 2 active fetches.

Log running and waiting counts observe-queue-load

const tasks = rows.map((row) => limit(() => saveRow(row)));
const interval = setInterval(() => {
  console.log({
    running: limit.activeCount,
    waiting: limit.pendingCount,
  });
}, 250);

await Promise.allSettled(tasks);
clearInterval(interval);

`activeCount` counts promises that started, while `pendingCount` counts queued functions that have not been called.

Open more slots on a busy limiter raise-live-concurrency

const limit = pLimit(2);
const tasks = records.map((record) => limit(() => writeRecord(record)));

if (limit.pendingCount > 100) {
  limit.concurrency = 6;
}

await Promise.all(tasks);

Setting concurrency to 6 lets more queued functions start. Reducing the value never interrupts promises already counted as active.

Collect successes and errors from one batch retain-task-failures

const outcomes = await Promise.allSettled(
  jobs.map((job) => limit(() => runJob(job)))
);

const errors = outcomes
  .filter((item) => item.status === 'rejected')
  .map((item) => item.reason);

A rejection does not drain p-limit's queue, and `Promise.allSettled()` keeps the outcome of every submitted job.

Pass a client and record without a closure forward-call-arguments

async function insert(client, record) {
  return client.records.create(record);
}

const tasks = records.map((record) =>
  limit(insert, database, record)
);

await Promise.all(tasks);

The callable limiter forwards all arguments to the task. The README treats this as a closure-allocation optimization for very large queues.

Give outer and inner work different limiters separate-nested-resources

const projectLimit = pLimit(2);
const assetLimit = pLimit(8);

await Promise.all(projects.map((project) =>
  projectLimit(async () => {
    const assets = await listAssets(project);
    return Promise.all(assets.map((asset) =>
      assetLimit(() => downloadAsset(asset))
    ));
  })
));

The README says nested calls to one full limiter can deadlock. Separate queues prevent outer jobs from occupying every slot needed by inner jobs.

Export one queue for several modules share-module-budget

// upstream-limit.js
import pLimit from 'p-limit';
export const upstreamLimit = pLimit(4);

// profiles.js
import {upstreamLimit as profileLimit} from './upstream-limit.js';
export const getProfile = (id) =>
  profileLimit(() => fetch(`/profiles/${id}`));

Every module importing `upstreamLimit` shares its 4 slots. Constructing another limiter creates another independent queue and allowance.

Lower concurrency after an HTTP 429 reduce-after-throttle

const limit = pLimit(8);

async function request(url) {
  const response = await fetch(url);
  if (response.status === 429) {
    limit.concurrency = Math.max(1, Math.floor(limit.concurrency / 2));
  }
  return response;
}

const responses = await limit.map(urls, request);

Changing concurrency after a 429 reduces later overlap, but p-limit still does not enforce a calls-per-interval quota.

Alternatives

PackageRegistryPick it when
p-queuenpmUse p-queue when the queue also needs priorities, pausing, events, interval caps, or a timeout for each operation.
p-throttlenpmChoose p-throttle for a rule that limits how many calls may start inside a time window.
p-mapnpmAn async iterator, backpressure, abort signals, or aggregate errors make p-map the better mapper.
bottlenecknpmBottleneck fits a Redis-backed rate or concurrency limit shared by several Node processes.

More utils guides

lru-cache · type-fest · ajv · find-up · js-yaml · zod · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.