promise-limit review
promise-limit 2.7.0 is a small FIFO gate for functions that return promises. Create a limiter with a concurrency count, submit zero-argument functions, and it starts only that many at once. The returned function also has map() for array work and queue for the number of jobs waiting to start. Our browser build was 1.3 KB minified and 0.6 KB gzipped. There are no runtime dependencies, priorities, time windows, cancellation hooks, or queue-clearing controls.
promise-limit 2.7.0 installed in 0.5 seconds as 1 package, and our bundle measured 0.6 KB gzipped with 0 audit findings. It remains reasonable for a frozen CommonJS service that needs FIFO concurrency only; new code should prefer p-limit when cancellation-adjacent queue controls and current maintenance matter.
We installed it
| Install | ✓ · 0.5s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 0.6 KB | gzipped (1.3 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does promise-limit install cleanly?
Yes. In a fresh container with an empty cache, npm install promise-limit finished in 0.5s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does promise-limit add to a browser bundle?
0.6 KB gzipped (1.3 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does promise-limit work with both ESM and CommonJS?
Yes. Both import 'promise-limit' and require('promise-limit') worked in Node 22 in our run. The package is published as CommonJS.
Does promise-limit include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
promise-limit or p-limit: which should you use?
p-limit: Use it for active and pending counts, queue clearing, mutable concurrency, and a currently maintained API. promise-limit 2.7.0 installed in 0.5 seconds as 1 package, and our bundle measured 0.6 KB gzipped with 0 audit findings.
When should you not use promise-limit?
Cancellation, timeouts, priorities, pause, rate windows, or clearing pending work are requirements; version 2.7.0 implements none of them.
Use it if
- A CommonJS service needs one local concurrency cap around promise-returning calls.
- Tasks should begin in submission order and results can be collected with Promise.all.
- A stream producer can use the exposed waiting count to apply its own backpressure.
- You need a 0.6 KB gzipped limiter and the package's fixed API already covers the job.
- Cancellation, timeouts, priorities, pause, rate windows, or clearing pending work are requirements; version 2.7.0 implements none of them.
- The mapper sometimes returns a plain value. map() calls .catch on the returned value, so its promise-returning contract is strict.
- Every mapped task must run after one fails. map() rejects on the first error and skips mapper calls that have not started.
- Active maintenance is a purchasing criterion. The latest npm version is from 2018 and GitHub's last push was July 18, 2022.
- A concurrency value of 0 should stop work. In this API, 0 and undefined mean unlimited execution.
Setup reality
We installed promise-limit 2.7.0 in a fresh Node 22 Bookworm sandbox in 0.5 seconds. The install left 1 package and 1 MB on disk, and npm audit found 0 known vulnerabilities. The package has 0 direct dependencies, 0 peer dependencies, 56 KB unpacked, an ISC license, and bundled TypeScript declarations. It is CommonJS without an exports map; require() and ESM import both worked under Node 22.23.2.
There are no credentials, config files, install scripts, or native modules. Give the limiter a function that starts the operation, such as limit(() => fetch(url)). Passing fetch(url) starts the request before the limiter sees it. Functions receive no arguments, so use a closure. Synchronous throws become rejected promises and release their slot.
Jobs start in the order submitted, while completion order depends on the work. queue counts waiting jobs only and excludes active jobs. The package does not bound that queue, pause a producer, expose activeCount, or provide shutdown. A fast input stream can still fill memory unless your code stops reading when queue crosses its own threshold.
The map() helper has a sharper failure contract than the basic limiter. Its mapper must return a real promise because version 2.7.0 calls .catch directly. After the first rejection it prevents queued mapper functions from running, although already-started calls continue. Our browser measurement was 1.3 KB minified and 0.6 KB gzipped, so size is not the reason to reject it; missing controls and old maintenance are.
Patterns
Cap parallel requests at 3 limit-concurrency
const promiseLimit = require('promise-limit');
const limit = promiseLimit(3);
const results = await Promise.all(
urls.map((url) => limit(() => fetchJson(url)))
);Version 2.7.0 needs a function that starts work; passing an already-created promise bypasses the 3-task cap.
Start jobs one at a time run-serially
const limit = require('promise-limit')(1);
await Promise.all([
limit(() => writeRecord('first')),
limit(() => writeRecord('second')),
limit(() => writeRecord('third')),
]);A concurrency of 1 preserves submission order, while Promise.all still rejects as soon as one returned promise rejects.
Capture arguments in the task closure pass-arguments
const limit = require('promise-limit')(4);
function lookup(id, locale) {
return limit(() => fetchUser(id, locale));
}The limiter invokes each function with 0 arguments, so the closure must carry id and locale.
Map with a fixed concurrency map-array
const limit = require('promise-limit')(5);
const pages = await limit.map(ids, (id, index) =>
fetchPage(id).then((page) => ({index, page}))
);The mapper must return a promise in version 2.7.0 because map() calls .catch on its result.
Keep output aligned with input preserve-result-order
const inputs = ['slow', 'fast', 'medium'];
const limit = require('promise-limit')(2);
const outputs = await Promise.all(
inputs.map((value) => limit(() => transform(value)))
);Promise.all returns values in input order even when 2 tasks finish in the opposite order.
Recover from one limited failure handle-task-error
const limit = require('promise-limit')(3);
const value = await limit(() => callService()).catch((error) => {
report(error);
return null;
});A rejection releases its slot before the returned promise rejects, allowing later queued work to start.
Treat a synchronous throw as rejection convert-sync-throw
await limit(() => {
if (!input.id) throw new Error('missing id');
return save(input);
});The implementation catches a synchronous exception, decrements the active count, and returns a rejected promise.
Watch jobs waiting to start inspect-waiting-count
for (const item of items) {
pending.push(limit(() => processItem(item)));
if (limit.queue > 100) await pauseProducer();
}
await Promise.all(pending);queue reports waiting jobs only; add the configured concurrency to estimate waiting plus running work.
Slow an async producer add-backpressure
for await (const record of source) {
while (limit.queue >= 50) {
await new Promise((resolve) => setTimeout(resolve, 25));
}
void limit(() => save(record)).catch(reportError);
}Version 2.7.0 does not pause the source automatically, so this 50-job threshold belongs to application code.
Select pass-through mode disable-the-cap
const limit = require('promise-limit')(shouldThrottle ? 8 : 0);
await Promise.all(tasks.map((task) => limit(task)));0 selects unlimited execution; it does not create a limiter with 0 available slots.
Handle map's early stop stop-map-after-error
try {
await limit.map(records, (record) => validateAndSave(record));
} catch (error) {
console.error('mapping stopped', error);
}Queued mapper calls are skipped after the first rejection, but tasks among the active 2 or 3 cannot be cancelled.
Run every task despite failures collect-all-results
const outcomes = await Promise.all(jobs.map((job) =>
limit(async () => {
try { return {status: 'fulfilled', value: await run(job)}; }
catch (reason) { return {status: 'rejected', reason}; }
})
));Wrap each task's error when all jobs must run; map() deliberately stops scheduling after its first rejected promise.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| p-limit | npm | Use it for active and pending counts, queue clearing, mutable concurrency, and a currently maintained API. |
| bottleneck | npm | Use it for rate windows, priorities, reservoirs, retries, or coordination across processes. |
| async-sema | npm | Use it when explicit acquire and release semantics fit resource pools or stream backpressure. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · 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.

