p-limit vs p-queue vs p-throttle
Concurrency and Rate Limiting in JavaScript
p-limitp-queuep-throttleSimilar Packages:

Concurrency and Rate Limiting in JavaScript

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
p-limit02,81814.9 kB0a month agoMIT
p-queue04,14576.8 kB52 months agoMIT
p-throttle051321.6 kB04 months agoMIT

Concurrency and Rate Limiting: p-limit vs p-queue vs p-throttle

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.

🚦 Execution Control Strategy: Count vs Time

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.

  • Once the limit is reached, new tasks wait in a hidden queue.
  • As soon as one finishes, the next starts.
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.

  • It tracks pending, active, and completed tasks.
  • Useful when you need to know when the queue is empty.
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.

  • It does not care about concurrency count, but rather time spacing.
  • Ideal for APIs with "X requests per second" rules.
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

πŸ—„οΈ Queue & State Management: Stateful vs Stateless

Managing the lifecycle of tasks often requires state. p-queue is stateful, while the others are stateless wrappers.

p-limit is stateless.

  • It returns a function wrapper.
  • You cannot pause or clear the internal queue directly.
  • To stop, you must manage external flags.
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.

  • It exposes methods to pause, resume, and clear the queue.
  • Essential for long-running workers or UI interactions.
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.

  • It maintains internal timers but exposes no queue controls.
  • You cannot drain or pause the throttle itself.
import pThrottle from 'p-throttle';

const throttled = pThrottle({ limit: 1, interval: 1000 });

// No pause/clear methods available
await throttled(() => doWork())();

πŸ“‰ Priority and Ordering

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.

  • No priority support.
  • Simple and predictable.
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.

  • Lower numbers usually mean higher priority.
  • Critical tasks can bypass less important ones.
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.

  • No priority support.
  • Focuses strictly on time spacing.
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'))();

⚠️ Error Handling & Resilience

How failures affect the flow differs slightly, though all propagate rejections.

p-limit propagates errors directly.

  • If a task fails, the returned promise rejects.
  • Other queued tasks continue normally.
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.

  • You can listen for error events globally.
  • Useful for logging without wrapping every task.
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.

  • Similar to p-limit, rejection is handled by the caller.
  • Throttling continues regardless of success/failure.
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

πŸ“Š Summary: Key Differences

Featurep-limitp-queuep-throttle
Primary GoalLimit concurrent countManage task queueLimit rate over time
StateStatelessStatefulStateless
Priority❌ Noβœ… Yes❌ No
Pause/Resume❌ Noβœ… Yes❌ No
Configconcurrencyconcurrency, timeoutlimit, interval
Best ForSimple concurrencyComplex workflowsAPI rate limits

πŸ’‘ The Big Picture

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.

How to Choose: p-limit vs p-queue vs p-throttle

  • p-limit:

    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.

  • p-queue:

    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.

  • p-throttle:

    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.

README for p-limit

p-limit

Run multiple promise-returning & async functions with limited concurrency

Works in Node.js and browsers.

Install

npm install p-limit

Usage

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);

API

pLimit(concurrency) default export

Returns a limit function.

concurrency

Type: number | object
Minimum: 1

Concurrency limit.

You can pass a number or an options object with a concurrency property.

rejectOnClear

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});

limit(fn, ...args)

Returns the promise returned by calling fn(...args).

fn

Type: Function

Promise-returning/async function.

args

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.

limit.map(iterable, mapperFunction)

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.

limit.activeCount

The number of promises that are currently running.

limit.pendingCount

The number of promises that are waiting to run (i.e. their internal fn was not called yet).

limit.clearQueue()

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().

limit.concurrency

Get or set the concurrency limit.

limitFunction(fn, options) named export

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);

fn

Type: Function

Promise-returning/async function.

options

Type: object

concurrency

Type: number
Minimum: 1

Concurrency limit.

rejectOnClear

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().

Recipes

See recipes.md for common use cases and patterns.

FAQ

How is this different from the 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.

Related

  • p-throttle - Throttle promise-returning & async functions
  • p-debounce - Debounce promise-returning & async functions
  • p-map - Run promise-returning & async functions concurrently with different inputs
  • p-all - Run promise-returning & async functions concurrently with optional limited concurrency
  • More…