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/20/2026
Here's the queuing mechanism, all implemented in index.js:
1. The queue itself
A Queue instance backs the limiter, and activeCount tracks how many tasks are currently running [index.js:L3-L16]:
const queue = new Queue();
let activeCount = 0;
2. Enqueueing a task
Every call to limit(fn, ...args) goes through generator, which wraps the call in a new Promise and immediately calls enqueue [index.js:L64-L67]:
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
enqueue(function_, resolve, reject, arguments_);
});
enqueue doesn't push the actual task function onto the queue — it pushes a queueItem whose run property is the resolve function of an inner promise. This inner promise, once resolved (i.e. "released" to run), triggers run(function_, resolve, arguments_) via .then [index.js:L46-L60]:
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 the internalResolve function rather than the task directly) is explicitly to preserve the async execution context across the queue boundary — noted in the comment at [index.js:L49-L50].
3. Releasing queued tasks
resumeNext is the gatekeeper — it only dequeues and starts a task if under the concurrency cap [index.js:L18-L23]:
const resumeNext = () => {
if (activeCount < concurrency && queue.size > 0) {
activeCount++;
queue.dequeue().run();
}
};
Calling .run() on the dequeued item resolves that inner promise, which unblocks the .then(run.bind(...)) chain set up in enqueue.
4. Actually executing the task
run invokes the real function, resolves the outer promise immediately with the resulting promise (so the caller isn't blocked waiting for completion), then awaits it just to know when to free up a concurrency slot [index.js:L27-L41]:
const run = async (function_, resolve, arguments_) => {
const result = (async () => function_(...arguments_))();
resolve(result);
try {
await result;
} catch {}
next();
};
5. Freeing a slot and cascading
When a task finishes, next decrements activeCount and calls resumeNext again, continuing the chain until the queue is empty or the limit is hit [index.js:L25-L28]:
const next = () => {
activeCount--;
resumeNext();
};
6. Dynamic concurrency changes
Setting limit.concurrency = n revalidates the value, then schedules a microtask that drains the queue up to the new limit [index.js:L92-L104]:
set(newConcurrency) {
validateConcurrency(newConcurrency);
concurrency = newConcurrency;
queueMicrotask(() => {
while (activeCount < concurrency && queue.size > 0) {
resumeNext();
}
});
},
This is exercised in tests like "change concurrency to smaller/bigger value" [test.js:L305-L339], which confirm that lowering concurrency mid-flight throttles new starts while running tasks continue, and raising it immediately drains more of the queue.
Supporting pieces
activeCountandpendingCount(=queue.size) are exposed as read-only getters on the returnedgeneratorfunction [index.js:L69-L74].clearQueue()empties the queue, optionally rejecting pending items with anAbortErrorifrejectOnClearwas set [index.js:L75-L86], as also documented in the README [readme.md:L107-L110] and demonstrated in the graceful-shutdown recipe.
Per the architecture signals, index.js is the most-depended-on file in the repo (3 importers) and the whole graph has 0 import cycles with an A-grade (100/100) score, so this queuing logic
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.