Show me how the concurrency limiter actually queues tasks.
Show me how the concurrency limiter actually queues tasks.
About sindresorhus/p-limit · 2 messages · last activity 7/27/2026
The core queuing logic lives in index.js. Here's the flow:
1. Setup
pLimit(concurrency) accepts either a number or an options object ({concurrency, rejectOnClear}), validates the concurrency value, and creates a Queue instance plus an activeCount counter starting at 0 [index.js:L3-L16].
2. Enqueuing a task
Every call to limit(fn, ...args) goes through the generator function, which wraps the call in a new Promise and immediately calls enqueue [index.js:L67-L69]:
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
enqueue(function_, resolve, reject, arguments_);
});
enqueue creates a queueItem holding a reject handler, then wraps the queue placement in another Promise whose resolve becomes queueItem.run [index.js:L48-L55]:
const enqueue = (function_, resolve, reject, arguments_) => {
const queueItem = {reject};
new Promise(internalResolve => {
queueItem.run = internalResolve;
queue.enqueue(queueItem);
}).then(run.bind(undefined, function_, resolve, arguments_));
if (activeCount < concurrency) {
resumeNext();
}
};
This indirection (queuing an internal resolve function rather than the run function directly) is deliberate — a comment notes it preserves the asynchronous execution context [index.js:L46-L47].
3. Dispatching queued items
resumeNext is the gatekeeper: it only pulls the next item off the queue if activeCount is under the concurrency limit and the queue isn't empty [index.js:L18-L24]:
const resumeNext = () => {
if (activeCount < concurrency && queue.size > 0) {
activeCount++;
queue.dequeue().run();
}
};
Calling .run() resolves that item's internal promise, which triggers the .then(run.bind(...)) chain from step 2 — actually invoking the task.
4. Running and completing a task
run executes the function, resolves the outer promise immediately with the in-flight result (so callers get their promise right away without waiting for completion), then awaits the result internally just to know when it's done, swallowing any error to avoid unhandled rejections [index.js:L26-L41]:
const run = async (function_, resolve, arguments_) => {
const result = (async () => function_(...arguments_))();
resolve(result);
try {
await result;
} catch {}
next();
};
next decrements activeCount and calls resumeNext again, continuing the cycle [index.js:L18-L24].
5. Dynamic concurrency changes
The concurrency property is a getter/setter defined via Object.defineProperties. Setting it validates the new value, then on the next microtask drains the queue up to the new limit [index.js:L91-L103]:
set(newConcurrency) {
validateConcurrency(newConcurrency);
concurrency = newConcurrency;
queueMicrotask(() => {
while (activeCount < concurrency && queue.size > 0) {
resumeNext();
}
});
},
This is exercised in the tests — e.g. shrinking concurrency from 4 to 2 mid-run [test.js:L305-L319] and growing it from 2 to 4 [test.js:L322-L339].
6. Clearing the queue
clearQueue() either just empties the queue, or, if rejectOnClear was set, rejects each pending item with an AbortError before discarding it [index.js:L74-L86].
index.js is also the most-depended-on file in the repo per the import graph (3 importers) [architecture signal], which fits since it's the single implementation module everything else (types, tests, benchmarks) revolves around.
FOLLOW_UPS:
- What does the internal
Queueclass look like? - How does
rejectOnCleardiffer from normalclearQueue()? - Why is the OpenSSF Scorecard rating so low?
Want to ask your own question?
Open chat for sindresorhus/p-limitPublic Q&A. Generated by RepoPilot from the actual source of sindresorhus/p-limit. AI answers can be incomplete or stale — verify before relying on them.