p-limit, p-queue, and p-throttle are utilities for controlling asynchronous execution in Node.js and browser environments. p-limit restricts the number of promises running concurrently. p-queue provides a priority queue with concurrency limits, timeouts, and state management. p-throttle limits the rate of function execution over a specific time interval. Together, they help prevent resource exhaustion, API rate limit violations, and performance bottlenecks.
When building robust JavaScript applications, uncontrolled asynchronous operations can lead to crashed servers, tripped API rate limits, or sluggish UIs. The p-* ecosystem offers three distinct tools to manage this flow: p-limit, p-queue, and p-throttle. While they all wrap async functions, they solve different problems. Let's compare how they handle execution control.
The core difference lies in what they limit: simultaneous active tasks or the frequency of tasks over time.
p-limit restricts the number of promises that are active at the same time.
import pLimit from 'p-limit';
const limit = pLimit(2); // Max 2 concurrent
const tasks = [1, 2, 3, 4].map(id =>
limit(() => fetch(`/api/item/${id}`))
);
await Promise.all(tasks);
// Only 2 fetches happen at once
p-queue also restricts concurrency but wraps it in a managed queue object.
import PQueue from 'p-queue';
const queue = new PQueue({ concurrency: 2 });
[1, 2, 3, 4].forEach(id => {
queue.add(() => fetch(`/api/item/${id}`));
});
await queue.onIdle();
// Waits until all tasks are done
p-throttle restricts how often a function can run over a time interval.
import pThrottle from 'p-throttle';
const throttled = pThrottle({
limit: 2,
interval: 1000 // 2 calls per 1000ms
});
const tasks = [1, 2, 3, 4].map(id =>
throttled(() => fetch(`/api/item/${id}`))()
);
await Promise.all(tasks);
// Calls are spaced out over time
Managing the lifecycle of tasks often requires state. p-queue is stateful, while the others are stateless wrappers.
p-limit is stateless.
import pLimit from 'p-limit';
const limit = pLimit(1);
let shouldStop = false;
const task = async () => {
if (shouldStop) return;
return limit(() => doWork());
};
// No built-in pause method
p-queue is stateful.
import PQueue from 'p-queue';
const queue = new PQueue();
queue.add(() => doWork());
queue.pause(); // Stops processing new items
queue.clear(); // Removes pending items
queue.start(); // Resumes processing
p-throttle is stateless.
import pThrottle from 'p-throttle';
const throttled = pThrottle({ limit: 1, interval: 1000 });
// No pause/clear methods available
await throttled(() => doWork())();
In complex systems, not all tasks are equal. Some need to jump the line.
p-limit processes tasks in FIFO (First-In-First-Out) order.
import pLimit from 'p-limit';
const limit = pLimit(1);
// First added runs first
limit(() => console.log('A'));
limit(() => console.log('B'));
p-queue supports priority levels.
import PQueue from 'p-queue';
const queue = new PQueue();
queue.add(() => console.log('Low'), { priority: 10 });
queue.add(() => console.log('High'), { priority: 1 });
// 'High' runs before 'Low' if both are waiting
p-throttle processes tasks in FIFO order.
import pThrottle from 'p-throttle';
const throttled = pThrottle({ limit: 1, interval: 1000 });
// First called runs first (when interval allows)
await throttled(() => console.log('A'))();
await throttled(() => console.log('B'))();
How failures affect the flow differs slightly, though all propagate rejections.
p-limit propagates errors directly.
import pLimit from 'p-limit';
const limit = pLimit(1);
try {
await limit(() => Promise.reject(new Error('Fail')));
} catch (err) {
console.error(err); // Caught here
}
// Next task in queue still runs
p-queue propagates errors but emits events.
error events globally.import PQueue from 'p-queue';
const queue = new PQueue();
queue.on('error', err => console.error(err));
queue.add(() => Promise.reject(new Error('Fail')));
// Error caught by listener, queue continues
p-throttle propagates errors directly.
p-limit, rejection is handled by the caller.import pThrottle from 'p-throttle';
const throttled = pThrottle({ limit: 1, interval: 1000 });
try {
await throttled(() => Promise.reject(new Error('Fail')))();
} catch (err) {
console.error(err);
}
// Throttle timer continues
| Feature | p-limit | p-queue | p-throttle |
|---|---|---|---|
| Primary Goal | Limit concurrent count | Manage task queue | Limit rate over time |
| State | Stateless | Stateful | Stateless |
| Priority | β No | β Yes | β No |
| Pause/Resume | β No | β Yes | β No |
| Config | concurrency | concurrency, timeout | limit, interval |
| Best For | Simple concurrency | Complex workflows | API rate limits |
p-limit is the lightweight choice πͺΆ. Use it when you just need to stop your code from opening 1000 database connections at once. It adds almost no overhead and requires minimal setup.
p-queue is the command center ποΈ. Use it when you need visibility and control. If you need to pause a batch job, prioritize urgent tasks, or wait for everything to finish before shutting down, this is the tool.
p-throttle is the metronome π΅. Use it when time is the constraint. If an API says "100 requests per minute," p-limit might still burst 100 requests in one second. p-throttle ensures they are spread out correctly.
Final Thought: While p-queue can technically replace p-limit (by setting concurrency), it brings extra weight. Conversely, p-throttle solves a problem the others cannot (time-based rate limiting). Choose the tool that matches your constraint: count, control, or time.
Choose p-limit when you need a lightweight, stateless solution to restrict the number of concurrent operations, such as limiting file uploads or database connections. It is ideal for simple scripts where you do not need to manage a queue state, pause execution, or prioritize tasks.
Choose p-queue for complex workflows requiring task prioritization, pause/resume capabilities, or completion events. It is best suited for batch processing jobs, workers that need to drain completely before shutdown, or scenarios where tasks arrive dynamically and need ordering.
Choose p-throttle when you need to enforce a rate limit over time, such as adhering to third-party API constraints (e.g., 5 requests per second). Use this when the timing of execution matters more than the strict concurrency count.
Run multiple promise-returning & async functions with limited concurrency
Works in Node.js and browsers.
npm install p-limit
import pLimit from 'p-limit';
const limit = pLimit(1);
const input = [
limit(() => fetchSomething('foo')),
limit(() => fetchSomething('bar')),
limit(() => doSomething())
];
// Only one promise is run at once
const result = await Promise.all(input);
console.log(result);
Returns a limit function.
Type: number | object
Minimum: 1
Concurrency limit.
You can pass a number or an options object with a concurrency property.
Type: boolean
Default: false
Reject pending promises with an AbortError when clearQueue() is called.
This is recommended if you await the returned promises, for example with Promise.all, so pending tasks do not remain unresolved after clearQueue().
import pLimit from 'p-limit';
const limit = pLimit({concurrency: 1});
Returns the promise returned by calling fn(...args).
Type: Function
Promise-returning/async function.
Any arguments to pass through to fn.
Support for passing arguments on to the fn is provided in order to be able to avoid creating unnecessary closures. You probably don't need this optimization unless you're pushing a lot of functions.
Warning: Avoid calling the same limit function inside a function that is already limited by it. This can create a deadlock where inner tasks never run. Use a separate limiter for inner tasks.
Process an iterable of inputs with limited concurrency.
The mapper function receives the item value and its index.
Returns a promise equivalent to Promise.all(Array.from(iterable, (item, index) => limit(mapperFunction, item, index))).
This is a convenience function for processing inputs that arrive in batches. For more complex use cases, see p-map.
The number of promises that are currently running.
The number of promises that are waiting to run (i.e. their internal fn was not called yet).
Discard pending promises that are waiting to run.
This might be useful if you want to teardown the queue at the end of your program's lifecycle or discard any function calls referencing an intermediary state of your app.
Note: This does not cancel promises that are already running.
When rejectOnClear is enabled, pending promises are rejected with an AbortError.
This is recommended if you await the returned promises, for example with Promise.all, so pending tasks do not remain unresolved after clearQueue().
Get or set the concurrency limit.
Returns a function with limited concurrency.
The returned function manages its own concurrent executions, allowing you to call it multiple times without exceeding the specified concurrency limit.
Ideal for scenarios where you need to control the number of simultaneous executions of a single function, rather than managing concurrency across multiple functions.
import {limitFunction} from 'p-limit';
const limitedFunction = limitFunction(async () => {
return doSomething();
}, {concurrency: 1});
const input = Array.from({length: 10}, limitedFunction);
// Only one promise is run at once.
await Promise.all(input);
Type: Function
Promise-returning/async function.
Type: object
Type: number
Minimum: 1
Concurrency limit.
Type: boolean
Default: false
Reject pending promises with an AbortError when clearQueue() is called.
This is recommended if you await the returned promises, for example with Promise.all, so pending tasks do not remain unresolved after clearQueue().
See recipes.md for common use cases and patterns.
p-queue package?This package is only about limiting the number of concurrent executions, while p-queue is a fully featured queue implementation with lots of different options, introspection, and ability to pause the queue.