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 8/1/2026
Now I have the actual queuing implementation. Here's how it works, all in pLimit [index.js:L3-L116]:
1. Setup
A Queue instance and an activeCount counter are created in the closure: const queue = new Queue(); let activeCount = 0; [index.js:L14-L15].
2. Enqueuing a call
Every time you call limit(fn, ...args), the returned generator function creates a Promise and calls enqueue [index.js:L69-L71]:
enqueuebuilds aqueueItemholding arejectcallback [index.js:L52].- It wraps the actual "start running" trigger in
new Promise(internalResolve => {...}), stashesinternalResolveasqueueItem.run, and pushes the item ontoqueueviaqueue.enqueue(queueItem)[index.js:L54-L57]. This is deliberate — queuing the resolve function rather than immediately runningfnpreserves the async execution context (e.g. forAsyncLocalStorage) of the caller. - That inner promise is chained with
.then(run.bind(undefined, function_, resolve, arguments_)), so once something callsqueueItem.run(), the realrunfunction executes [index.js:L58]. - Finally, if
activeCount < concurrency, it immediately callsresumeNext()to try to start something right away [index.js:L60-L62].
3. Dispatching from the queue
resumeNext is the gatekeeper [index.js:L17-L22]:
if (activeCount < concurrency && queue.size > 0) {
activeCount++;
queue.dequeue().run();
}
It only pulls one item off the queue (FIFO, via queue.dequeue()) when there's spare concurrency, increments activeCount, and calls that item's .run() — which resolves the earlier internal promise and lets the chained .then(run...) actually invoke the user's function.
4. Running the function
run [index.js:L27-L40] executes function_(...arguments_) wrapped in an async IIFE, immediately resolves the outer promise returned to the caller with that result promise (so callers don't wait for internal bookkeeping), then awaits it (swallowing errors just to avoid unhandled rejections — the real rejection still propagates to the caller via the resolved promise), and finally calls next().
5. Freeing a slot
next [index.js:L24-L26] decrements activeCount-- and calls resumeNext() again, which lets the next queued item run if capacity allows — this is the recursive mechanism that keeps the queue draining one at a time as slots free up.
6. Dynamic concurrency changes
The concurrency setter [index.js:L91-L102] validates and updates concurrency, then schedules a queueMicrotask that loops while (activeCount < concurrency && queue.size > 0) resumeNext(); — so raising the limit immediately drains more queued items, as shown in the test 'change concurrency to bigger/smaller value' [test.js:L305-L339].
7. Clearing the queue
clearQueue [index.js:L79-L89] either just calls queue.clear(), or, if rejectOnClear was set, dequeues everything and calls .reject(abortError) on each pending item using an AbortSignal.abort().reason.
This design (queuing a resolver rather than a runnable task) is what lets pendingCount simply report queue.size [index.js:L75-L77] and activeCount track in-flight function calls [index.js:L72-L74].
FOLLOW_UPS:
- What data structure does the internal
Queueclass use? - How does
rejectOnClearproduce theAbortError? - How is async execution context preserved across queued tasks?
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.