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 9/1/2026
The core queuing logic lives in [index.js:L3-L116]. Here's how it flows:
1. Setup
pLimit(concurrency) accepts either a number or an options object (destructuring concurrency and rejectOnClear), validates the concurrency value, and creates a Queue instance plus an activeCount counter [index.js:L3-L14].
2. The queue advancement logic
resumeNext is the gatekeeper — it only dequeues and runs a task if activeCount < concurrency and the queue is non-empty:
const resumeNext = () => {
if (activeCount < concurrency && queue.size > 0) {
activeCount++;
queue.dequeue().run();
}
};
[index.js:L18-L23]
next() is called when a task finishes — it decrements activeCount and calls resumeNext() to pull in the next queued item [index.js:L25-L28].
3. Running a task
run actually invokes the user's function, immediately resolves the outer promise with the resulting promise (so the caller doesn't wait for completion to get the promise reference), then awaits it internally just to know when to decrement activeCount via next(). Errors are swallowed here specifically to avoid unhandled rejections, while the real rejection is still delivered to the caller through the resolved promise [index.js:L30-L45].
4. Enqueuing
enqueue is where a task actually joins the queue. It wraps the "run" trigger in an internal Promise whose resolve function is stored as queueItem.run — this preserves the async execution context when the item is later dequeued and resumed. The queueItem (with reject attached) is pushed via queue.enqueue(queueItem). After queuing, if activeCount < concurrency, it immediately calls resumeNext() to try to start it right away [index.js:L47-L59].
5. Public entry point
The generator function (what you get back as limit) wraps everything in a new Promise and calls enqueue with resolve/reject from that promise plus the function and its arguments [index.js:L61-L63].
Concurrency changes on the fly
The concurrency property has a setter that revalidates the new value, updates it, then uses queueMicrotask to drain the queue up to the new limit by repeatedly calling resumeNext() while activeCount < concurrency and items remain [index.js:L92-L104].
Clearing the queue
clearQueue() either just calls queue.clear() (default), or, if rejectOnClear was enabled, dequeues everything and rejects each pending item with an AbortError derived from AbortSignal.abort().reason [index.js:L75-L86]. This matters because, per the README, pending promises would otherwise never settle after a queue clear if you didn't opt into rejectOnClear [readme.md:L47-L50].
Note: the actual Queue data-structure implementation (its enqueue/dequeue/clear/size) isn't shown in the retrieved chunks — only its usage. I can search for it if you want the internal FIFO details.
FOLLOW_UPS:
- Show the internal Queue data structure implementation
- How does the concurrency setter drain the queue safely?
- Explain the rejectOnClear AbortError behavior in detail
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.