# p-queue: A Promise Queue with Concurrency Control for Node.js

> p-queue limits how many async operations run at once, with priority, timeouts and two rate-limiting modes. It is feature complete, ESM-only, and aimed at in-process throttling rather than durable job processing.

**sindresorhus/p-queue** — Promise queue with concurrency control

- Repository: https://github.com/sindresorhus/p-queue
- Stars: 4,277 · Forks: 221
- Language: TypeScript
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/sindresorhus-p-queue

## The problem p-queue solves: too many promises in flight

Node.js will happily start every promise you create. If you map over a list of URLs and fire a request for each one, you get all of them at once. Some APIs answer that with HTTP 429. Some processes answer it with memory pressure. p-queue exists to put a ceiling on the number of operations running at the same time, and optionally on how many run per time interval.

The README frames the target use case directly: rate-limiting async or sync operations, for example when interacting with a REST API or when doing CPU or memory intensive tasks. The package is a single in-process object. You create a queue, you add functions to it, and the queue decides when each function starts. Nothing leaves the process, and nothing survives a restart.

That scope matters when choosing it. The same README states that for servers you probably want a Redis-backed job queue instead, and links to a list of job queues. p-queue is for the concurrency problem inside one running program, not for work that must be retried, persisted or distributed across machines.

## How the queue schedules work: concurrency, priority and intervals

A PQueue instance is constructed with an options object. The concurrency option defaults to Infinity and has a minimum of 1; the README example sets it to 1 to run one promise at a time, and notes that setting it to 4 runs four at once. Each call to queue.add(fn) returns a promise that settles when the task completes, and that promise resolves with the return value of fn. The README carries an explicit warning about this: if you await the promise returned by add, you wait for the task to finish, which can defeat the purpose of using a queue for concurrency. The documented pattern is to call add and let the returned promise settle separately.

Ordering is not strictly first-in-first-out. Tasks accept a priority number, defaulting to 0, and operations with greater priority are scheduled first. A task can also carry an id, a string used to update its priority before execution; if you do not supply one, the queue assigns an incrementing BigInt starting from 1n.

Rate limiting is a separate axis from concurrency. The intervalCap option caps the number of runs in an interval, and interval sets the length of that interval in milliseconds before the count resets. With the default strict: false, the count resets at fixed window boundaries, so tasks can burst across a boundary. The README gives a concrete case: with intervalCap: 2 and interval: 1000, you could execute 2 tasks at 999ms and 2 more at 1000ms, four tasks within 1ms. Setting strict: true switches to a sliding window that tracks individual execution timestamps, so no more than intervalCap tasks run in any rolling interval. The README notes strict mode is more resource-intensive, and that carryoverIntervalCount has no effect when strict is enabled because strict mode tracks execution timestamps rather than counting pending tasks.

The queue also supports a queueClass option: a class with enqueue and dequeue methods and a size getter, which lets you replace the internal data structure.

## Installing p-queue and running a first throttled task

The package installs from npm. The README gives one install command and one warning: the package is native ESM and no longer provides a CommonJS export, so CommonJS projects must convert to ESM. The README also asks that issues about CommonJS and ESM not be opened. The package.json sets "type": "module" and declares engines of node >=20, so Node.js 20 or later is the floor.

```bash
npm install p-queue
```

The README's usage example imports PQueue, constructs a queue with concurrency 1, and adds two async tasks. Each task fetches a URL and logs a line when it finishes. Because concurrency is 1, the second task starts only after the first settles.

```js
import PQueue from 'p-queue';
import got from 'got';

const queue = new PQueue({concurrency: 1});

(async () => {
	await queue.add(() => got('https://sindresorhus.com'));
	console.log('Done: sindresorhus.com');
})();

(async () => {
	await queue.add(() => got('https://avajs.dev'));
	console.log('Done: avajs.dev');
})();
```

Note what is awaited here: the outer async functions await the add call, but the two IIFEs are not awaited by anything, so they run concurrently while the queue serialises the actual requests. If you need a per-task deadline, the timeout option sets a per-operation timeout in milliseconds, and tasks that exceed it throw a TimeoutError. The timer starts when the operation is dequeued and begins execution, not while it waits in the queue. A task can override the queue-level value:

```js
const queue = new PQueue({timeout: 5000});

// This task uses the global 5s timeout
await queue.add(() => fetchData());

// This task has a 10s timeout
await queue.add(() => slowTask(), {timeout: 10000});
```

For cancellation, add accepts a signal option, an AbortSignal. When the signal aborts, the task is removed from the queue and the add call rejects with the signal's reason. If the operation is already running, the signal must be handled by the operation itself. The README shows a got example that listens for the abort event and calls request.cancel().

## Error handling and the unhandled rejection trap

The README is blunt about the failure mode here: if your items can potentially throw an exception, you must handle those errors from the returned promise, or they may be reported as an unhandled promise rejection and potentially cause your process to exit immediately. Because the recommended pattern is not to await the promise returned by add, it is easy to write code where the rejection has no handler attached. That is a real operational risk, not a theoretical one, and it is the first thing to check when a process dies without an obvious stack trace.

The timeout behaviour is another boundary. A TimeoutError fires only after a task has started executing, so a long queue wait is not covered by the timeout option. If your concern is total latency rather than execution time, the queue itself will not enforce it.

The strict rate-limiting mode is the third trade-off. It gives predictable, evenly distributed execution, which matters for APIs that enforce strict limits, but the README states it is more resource-intensive because it tracks individual execution timestamps. Choosing strict mode is a deliberate cost, not a free upgrade.

## When p-queue is the wrong tool

The README's own guidance is the clearest boundary: for servers, you probably want a Redis-backed job queue instead. p-queue holds tasks in memory. If the process restarts, queued work is gone. There is no persistence, no retry policy, no dead-letter handling and no cross-process coordination in the documented API. A queue that must survive a deploy is a different category of software.

A second boundary is module format. The package is native ESM with no CommonJS export. A CommonJS codebase that cannot convert to ESM cannot use this version at all, and the README explicitly declines to answer questions about that.

A third is scope. The README states the project is feature complete, that pull requests are welcome but no further development is planned, and that email support questions are not answered. If you need a maintained roadmap, this is not it. The last push to the repository was on 2026-07-22, and the most recent release listed is v9.3.3 on the same date. That is recent, but the stated intent is stability rather than new features, so evaluate it as a finished component.

## p-queue vs p-limit and other alternatives

The related searches around this package include p-limit, p-throttle and better-queue, and the comparison that matters most is p-limit. Both are from the same author and both cap concurrency, but the shape differs. p-limit is a function wrapper: you create a limiter and wrap individual functions so that calls to that wrapper are throttled. p-queue is an explicit queue object that you add tasks to, and that object carries state and events. The queue is an EventEmitter3 subclass, so you can observe it, and it exposes priority, per-task ids for priority updates, interval-based rate limiting, timeouts and abort signals. If you only need a concurrency ceiling around a function, p-limit is the smaller tool. If you need ordering by priority, rate limits per interval, or introspection of a queue, p-queue is the one with those mechanisms.

The searches also mention bullmq and Redis-backed job queues generally, but that is a different problem class rather than a competing implementation: those persist jobs outside the process and coordinate multiple workers, which p-queue does not do by design.

## Licence, maintenance and upgrade cost

p-queue is MIT licensed, per the repository's licence file and the license field in package.json. That is a permissive licence, and the practical implication for most teams is that you can use it in closed-source software provided you keep the copyright notice and licence text. This is a description of the licence, not legal advice; have your own counsel review if your distribution model is unusual.

The runtime dependency list is short: eventemitter3 and p-timeout. Everything else in package.json is a devDependency used for building, testing and benchmarking. That keeps the install surface small.

The upgrade cost is dominated by the ESM constraint and the Node.js floor of 20. Both are declared in package.json, and both are hard boundaries rather than gradual migrations. The README's release history shows patch releases in the 9.3.x line through July 2026, so the current major is stable, and the feature-complete statement means you should not expect the API to grow. Budget for the ESM conversion once, and expect few surprises afterwards.

## Conclusion

Adopt p-queue when you need to cap concurrent async calls inside one Node.js process, especially for REST APIs or CPU-heavy work, and you are ready for an ESM-only dependency. Do not adopt it as a durable job queue for servers; the README points to a Redis-backed job queue for that role. Before installing, check that your project runs Node.js 20 or later, that your build can consume a native ESM package with no CommonJS export, and that the fixed-window rate limiting is acceptable, or set strict to true and accept the extra timestamp tracking.

## FAQ

### What is p-queue?

It is a promise queue with concurrency control, published on npm, that limits how many async or sync operations run at the same time and optionally per time interval. The README describes it as useful for rate-limiting operations such as REST API calls or CPU and memory intensive tasks.

### How does p-queue compare with bullmq?

The README does not compare the two. It does state that for servers you probably want a Redis-backed job queue instead, and bullmq is a Redis-backed job queue. p-queue keeps tasks in memory inside one process, while a Redis-backed queue persists and distributes jobs.

### How do I install p-queue?

The README gives a single command: npm install p-queue. Note the warning that the package is native ESM and no longer provides a CommonJS export, so CommonJS projects must convert to ESM first.

### Does p-queue work in CommonJS projects?

No. The README states the package is native ESM and no longer provides a CommonJS export, and it asks that issues about CommonJS and ESM not be opened. CommonJS users have to convert their project to ESM.

### How do I set a timeout for a task in p-queue?

Pass the timeout option in milliseconds either to the PQueue constructor or to an individual add call, which overrides the queue-level value. The timer starts when the operation is dequeued and begins execution, not while it waits in the queue, and the task throws a TimeoutError if it does not finish in time.

## Sources

- [Issues](https://github.com/sindresorhus/p-queue/issues)
- [License: MIT](https://github.com/sindresorhus/p-queue/blob/main/LICENSE)
- [README](https://github.com/sindresorhus/p-queue/blob/main/README.md)
- [Releases](https://github.com/sindresorhus/p-queue/releases)
- [sindresorhus/p-queue on GitHub](https://github.com/sindresorhus/p-queue)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sindresorhus-p-queue
