p-queue
Promise queue with concurrency control, interval-based rate limiting, and priority scheduling for async tasks.
Repository Health
Technical Analysis
p-queue is a promise queue for Node.js and modern browsers that runs async (or sync) functions with a configurable concurrency limit. Instead of firing every task at once, callers .add() work to the queue and p-queue dequeues it as capacity frees up, making it a common building block for rate-limiting calls to REST APIs, throttling CPU/memory-intensive jobs, or capping parallel file operations.
Beyond a basic concurrency cap, it supports interval-based throttling (intervalCap + interval, with an optional sliding-window strict mode), per-task priority so higher-priority work jumps the queue, per-task or per-queue timeouts via p-timeout, and AbortSignal cancellation. It exposes an EventEmitter3-based event surface (active, idle, empty, next, completed, error, rateLimit, and more) plus promise-returning helpers like .onIdle(), .onEmpty(), and .onSizeLessThan() for coordinating queue state without polling.
What You Get
- A
PQueueclass you instantiate withconcurrency,intervalCap/interval,timeout, andautoStartoptions, then feed work to via.add()or.addAll() - A pluggable
queueClass— the defaultPriorityQueuekeeps a sorted, amortized-O(1) array so higher-prioritytasks run first, and you can swap in your own class implementingenqueue/dequeue/size - Interval-based throttling with a fixed-window default and an opt-in
strictsliding-window mode that tracks execution timestamps to prevent bursts at window boundaries - Promise-returning state hooks —
.onEmpty(),.onIdle(),.onPendingZero(),.onRateLimit(),.onRateLimitCleared(),.onSizeLessThan(),.onError()— so you canawaitqueue transitions instead of listening for events - Per-task cancellation via
AbortSignaland per-task or per-queuetimeout(backed byp-timeout, raisingTimeoutError) without writing your own timer bookkeeping
Common Use Cases
- Rate-limiting calls to a third-party REST API that enforces a requests-per-second cap
- Throttling CPU- or memory-intensive tasks (image processing, file I/O) to a fixed number of parallel workers
- Reordering in-flight work by priority — e.g. serving interactive requests ahead of background batch jobs sharing the same worker pool
- Coordinating multiple producers adding work over time by awaiting
.onIdle()or.onEmpty()instead of managing a manual counter
Under The Hood
Architecture
Execution flows from a single public entry point, PQueue in source/index.ts, which owns all scheduling state (concurrency counters, interval timers, pending-task bookkeeping) and delegates ordering to an injected queue implementation via the queueClass option — source/priority-queue.ts by default. This separation of concerns means the class enforces concurrency, timeout, and rate-limit invariants directly, while pluggable, encapsulated logic (implementing the small Queue interface from source/queue.ts) decides what runs next; swapping queueClass changes ordering strategy without touching any scheduling code. Interval-based rate limiting runs as a small state machine of its own (#initializeIntervalIfNeeded, #onInterval, recurring vs one-shot timers) layered on top of the same dequeue loop the plain-concurrency path uses, so strict sliding-window mode extends rather than forks the core scheduling logic. The design is intentionally a flat orchestrator plus one swappable strategy object, not a deep layered architecture — reasonable for a focused primitive, and it means the core abstraction that would break the most if changed is the Queue interface’s contract (enqueue/dequeue/size/setPriority/remove), since every scheduling decision funnels through it.
Tech Stack
Written in strict TypeScript targeting Node.js 20+, compiled with tsc against @sindresorhus/tsconfig, and shipped as ESM-only (no CommonJS export, "type": "module"). Runtime dependencies are minimal and purposeful: eventemitter3 supplies the typed event-emitter base class PQueue extends, and p-timeout implements the per-task/per-queue timeout wrapping (including its TimeoutError). There is no build framework beyond tsc plus del-cli for cleaning dist/; the package ships only compiled dist/ output per its files field. CI (.github/workflows) runs the test suite via npm test across a Node 20/24 matrix on GitHub Actions.
Code Quality
Testing uses Node’s built-in node:test runner (via tsx/esm) across seven dedicated files (basic.ts, advanced.ts, strict.ts, rate-limit.ts, priority-queue.ts, validation.ts, debug.ts) totaling well over 100KB of test code, exercising concurrency edge cases, timing-sensitive rate-limit behavior, priority ordering, and input validation with real timers and assert/strict. Linting is enforced through xo (a strict, opinionated ESLint preset) with only a small, explicitly justified set of TypeScript rule overrides, and tsd runs type-level tests against the public .d.ts output in test-d/ to guard the published type signatures themselves, not just runtime behavior. Source code favors private class fields (#), explicit TypeError throws with descriptive messages for invalid options, and inline comments explaining non-obvious invariants (e.g. the circular-buffer bookkeeping in #cleanupStrictTicks) rather than leaving them undocumented.
API Design
The public surface is a single constructor plus a small, consistently-named method set (.add(), .addAll(), .pause(), .start(), .clear()) paired with a family of on*() promise-returning state hooks (.onIdle(), .onEmpty(), .onPendingZero(), .onRateLimit(), .onSizeLessThan()) that let callers await queue transitions instead of wiring up event listeners by hand, while the underlying EventEmitter3 events remain available for repeated-notification cases. Getting started requires no boilerplate beyond new PQueue(options) and .add(fn); every option (concurrency, intervalCap, interval, timeout, priority) is optional with a sensible default, and JSDoc on each option in source/options.ts includes inline usage examples. The README documents every corner (custom queueClass, AbortSignal cancellation, setPriority) with runnable code samples, keeping the learning curve low despite the library’s fairly deep feature set.
Used by 22 apps in this directory
AnythingLLM
Developer Tools · Automation · AI Assistants
The all-in-one AI platform for private document chat, no-code agents, and local LLMs with zero setup friction.
Bun
Developer Tools
An all-in-one JavaScript and TypeScript toolkit — one Rust-and-JavaScriptCore binary that replaces Node.js, npm, a bundler, and a test runner with faster equivalents.
Cherry Studio
AI Assistants
All-in-one AI desktop client with 300+ assistants and multi-model support
CodeSandbox
Code Editors · Developer Tools
Instantly ready browser-based IDE that runs full npm dependency resolution and transpilation entirely client-side, with no server needed.
Directus
CMS · Low Code Platforms
Connect any SQL database and get instant REST and GraphQL APIs, a visual management Studio, and a native MCP server for AI agents — free for most organizations.
GrowthBook
Developer Tools · Analytics · Monitoring
Open source feature flags, A/B testing, and warehouse-native experimentation that queries your existing data infrastructure—no data movement required.
GraphQL Hive
Developer Tools · Devops · Monitoring
Open-source GraphQL schema registry and observability platform with breaking change detection, federation support, and CI/CD integration for teams of any size.
Huly Platform
Project Management · Team Chat · Collaboration
Open-source all-in-one workspace that replaces Linear, Jira, Slack, and Notion for product and engineering teams.
Jitsu
Data Engineering
Open-source, fully-scriptable data ingestion engine that streams events from web, apps, and APIs to any data warehouse in real time.