p-limit: capping promise concurrency without a full queue
Run multiple promise-returning & async functions with limited concurrency
At a glance
- What is it?
- p-limit does one thing: it holds back promise-returning functions so only N run at once. This review covers its API surface, the deadlock trap in its own docs, and when p-queue or p-map is the better fit.
- Who is it for?
- Adopt p-limit when you need a hard ceiling on in-flight async work inside a single process and nothing more: it is a small, ESM-only module with one runtime dependency and an MIT licence. Do not adopt it if you need to pause a queue, inspect individual jobs, or retry failures; the README points at p-queue for that, and p-map for mapping over inputs.
- 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 11 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 September 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What p-limit actually solves
An unbounded Promise.all over a thousand URLs opens a thousand sockets at once. p-limit exists to put a ceiling on that. The README describes it as a way to "run multiple promise-returning & async functions with limited concurrency", and that is the whole scope: you give it a number, it gives you back a wrapper function, and only that many wrapped calls execute at a time.
The audience is narrow and specific. It is for Node.js or browser code that already has a list of async operations and needs to slow the burst down: scraping, hitting a rate-limited API, reading many files without exhausting file descriptors. If your problem is scheduling, prioritisation, or pausing, this is not the tool, and the README says so directly in its FAQ.
The limiter, the queue, and the counters behind it
The package has one runtime dependency, yocto-queue, which holds the pending work. Calling pLimit(concurrency) returns a limit function. Each call to limit(fn, ...args) either runs fn immediately, if fewer than concurrency promises are active, or pushes an entry onto the queue. When a running promise settles, the limiter pulls the next entry and invokes it.
Two counters expose that state. limit.activeCount reports promises currently running; limit.pendingCount reports entries whose internal function has not been called yet. limit.concurrency is readable and writable, so you can raise or lower the ceiling at runtime without rebuilding the limiter.
There is also a second, less obvious export. limitFunction(fn, options) wraps a single function so that repeated calls to it share one concurrency budget, rather than one budget shared across many different functions. The README frames the distinction as controlling "the number of simultaneous executions of a single function".
One design detail worth noting: limit(fn, ...args) forwards arguments to fn so you can avoid allocating a closure per task. The README calls this an optimisation you "probably don't need" unless you are pushing a lot of functions. That is an honest framing, and it is the kind of detail that usually gets oversold.
Install and first run with p-limit
The README gives a single install command. It requires Node.js 20 or newer per the engines field in package.json, and the package is ESM-only: package.json sets "type": "module" and exports only index.js and index.d.ts, with no CommonJS entry point.
npm install p-limitAfter installing, the minimal usage from the README wraps three calls and awaits them together. With a concurrency of 1, only one promise runs at a time.
import pLimit from 'p-limit';
const limit = pLimit(1);
const input = [
limit(() => fetchSomething('foo')),
limit(() => fetchSomething('bar')),
limit(() => doSomething())
];
const result = await Promise.all(input);
console.log(result);The reader should expect result to be an array in the same order as input, matching the resolved values of the three functions. The ordering of results is the array order, not completion order.
For a batch of inputs, limit.map(iterable, mapperFunction) is the shorter form. The mapper receives the item and its index, and the README states the return value is equivalent to Promise.all over Array.from(iterable, (item, index) => limit(mapperFunction, item, index)). If the iterable throws mid-iteration, the returned promise rejects with that error, and mapper functions already scheduled still run with their results discarded.
The deadlock the README warns about
The most important line in the API documentation is a warning, not a feature. If a function that is already running under a limiter calls the same limiter again, the inner task can never start, because the outer task is holding the slot it is waiting on. The README states this plainly: avoid calling the same limit function inside a function that is already limited by it, and use a separate limiter for inner tasks.
This is a genuine failure mode and it is easy to hit in recursive code, such as a crawler that follows links from inside a limited fetch. The failure is silent in the sense that nothing throws; the promise simply never settles. A second, related trap is clearQueue(). It discards pending promises, but by default those promises are left unresolved, so if you await them with Promise.all, that await hangs. The rejectOnClear option exists for exactly this case, and the README recommends it when you await the returned promises. The default is false, so the safe behaviour is opt-in.
Finally, clearQueue() does not cancel work that is already running. There is no cancellation mechanism in the API at all. If you need to abort in-flight operations, that is your own AbortController wiring inside the limited function, not something the limiter provides.
p-limit against p-queue, p-map and plain Promise.all
The README answers the p-queue comparison itself: p-limit is "only about limiting the number of concurrent executions", while p-queue is a full queue implementation with more options, introspection, and the ability to pause. If you need to pause and resume a backlog, inspect queued jobs individually, or apply per-job priorities, p-queue is the appropriate dependency and p-limit will feel like a missing feature set rather than a lightweight choice.
The difference from p-map is about shape rather than scheduling. p-map takes an iterable and a mapper and returns mapped results; p-limit hands you a wrapper you compose yourself. limit.map is a convenience the README offers for "inputs that arrive in batches", and it explicitly points at p-map for more complex cases. If your work is one list in, one list out, p-map is the more direct expression; if you are wrapping calls scattered across a codebase, p-limit's wrapper fits better.
Against plain Promise.all, the difference is the ceiling. Promise.all starts everything immediately and rejects on the first failure. p-limit keeps everything running but throttled, and still rejects through Promise.all if one limited function rejects. It does not retry, does not reorder, and does not isolate failures.
Maintenance, licence and the upgrade surface
The repository is not archived, and the last push was on 2026-09-18, the same day as the v7.3.3 release. Recent releases land at a steady but unhurried pace: v7.3.1 on 2026-07-20, v7.3.2 on 2026-08-31, v7.3.3 on 2026-09-18. Patch-level versioning across those three releases suggests the API is stable, but the version number itself is a signal about upgrade cost: this is major version 7, and the ESM-only packaging means a major bump is the point at which CommonJS consumers get cut off.
The licence is MIT, which permits commercial and private use with the copyright notice retained. That is a permissive arrangement, but it is not legal advice and the licence file in the repository is the authority.
The practical upgrade cost is low in normal use. The public surface is small: a default export, one named export, three properties, and two methods. The dependency footprint is one package, yocto-queue. The risk sits in the packaging rather than the API: if a future major moves the minimum Node version again, or drops a method, the migration is a version bump plus a search for the affected call sites. Pinning the major version is the cheap insurance here.
Editorial conclusion
Adopt p-limit when you need a hard ceiling on in-flight async work inside a single process and nothing more: it is a small, ESM-only module with one runtime dependency and an MIT licence. Do not adopt it if you need to pause a queue, inspect individual jobs, or retry failures; the README points at p-queue for that, and p-map for mapping over inputs. Before you commit, verify three things in your own tree: that every runtime touching this package is on Node 20 or newer, that your bundler or loader handles an ESM-only package with no CommonJS build, and that no limited function calls its own limiter, because the README warns that this deadlocks inner tasks.
Frequently asked questions
What is p-limit?
It is an npm package that runs promise-returning and async functions with a limited concurrency, working in Node.js and browsers. You create a limiter with pLimit(concurrency) and wrap each call with the returned function.
How do I use p-limit?
Import the default export, call pLimit with a concurrency number, wrap each async call in the returned limit function, then await them together with Promise.all. The README's example uses pLimit(1) so only one promise runs at once.
How is p-limit different from p-queue?
The README states that p-limit only limits the number of concurrent executions, while p-queue is a full queue implementation with more options, introspection, and the ability to pause the queue.
How is p-limit different from p-map?
p-map takes an iterable and a mapper function and returns mapped results. p-limit gives you a wrapper function you apply to individual calls, and its limit.map method is described in the README as a convenience for inputs that arrive in batches, with a pointer to p-map for more complex cases.
How is p-limit different from Promise.all?
Promise.all starts every promise immediately. p-limit keeps only the configured number of promises running at a time and queues the rest, while results are still collected through Promise.all over the wrapped 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-limit)