queue

Tiny, dependency-free async queue with concurrency control — faster and smaller than p-limit or fastq, with optional rate limiting.

Library
npm
v1.2.0
152stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
34/100Needs Attention
Development Activity0
Maintenance32
Community28
Maturity48
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
60/100Good
Architecture78
Code Quality70
Innovation58
Learning Curve35

@henrygd/queue is a minimal JavaScript/TypeScript library for controlling how many async tasks run at the same time. Create a queue with a concurrency limit, add promise-returning functions to it with add(), and the queue runs them in FIFO order without exceeding that limit — useful for throttling API calls, batch-processing large arrays, or preventing resource exhaustion in Node, Deno, Bun, Cloudflare Workers, and browsers alike.

Beyond plain concurrency limiting, the package ships two additional entry points: @henrygd/queue/rl adds time-based rate limiting (cap how many tasks start per time window, not just how many run concurrently) for working with rate-limited APIs, and @henrygd/queue/async-storage binds each queued task to Node’s AsyncResource/AsyncLocalStorage context so request-scoped state survives the queue hop. The author benchmarks the library extensively against p-limit, fastq, async.queue, promise-queue, and queue, consistently coming out faster and smaller (472 bytes minified for the core queue).

What You Get

  • Concurrency-limited queue via newQueue(concurrency) with add, all, done, clear, active, and size methods
  • Optional time-based rate limiting through the @henrygd/queue/rl entry point (newQueue(concurrency, rate, interval))
  • AsyncLocalStorage/AsyncResource-aware queue via @henrygd/queue/async-storage for Node contexts that need to preserve async context across queued calls
  • Promise.all-style batch helper (queue.all()) that queues an array of promises or promise-returning functions and resolves once they’re all done
  • Cross-runtime support for Node.js, Deno, Bun, Cloudflare Workers, and browsers with ESM and CJS builds plus TypeScript types

Common Use Cases

  • Throttling concurrent API requests to a third-party service so you don’t overwhelm it
  • Batch-processing large datasets (e.g. paginated fetches) a fixed number at a time instead of all at once
  • Limiting concurrent file or database operations to avoid exhausting connections/handles
  • Adding client-side rate limiting when calling APIs with strict per-second rate limits
  • Wrapping worker/task execution in serverless or edge environments (Cloudflare Workers) where resource limits are tight

Under The Hood

Architecture The library exports a factory function newQueue(concurrency) returning a closure-based Queue object backed by a singly-linked list (Node types with p/res/rej/next) rather than an array, avoiding shift-cost overhead. State (head/tail/active/size) lives in closure variables rather than a class instance, keeping the returned object literal small and avoiding prototype lookups. The core run() function drains the queue while active < concurrency, invoking each queued function and chaining .then() to resolve/reject the original promise and decrement active via afterRun(), which either continues draining or resolves a pending done() promise once size hits zero. Three near-identical variants (index.ts, index.rl.ts for rate limiting, index.async-storage.ts for AsyncResource binding) are maintained as separate files rather than composed — a deliberate size-over-DRY tradeoff so each entry point stays independently tiny for bundlers. Any refactor of the core linked-list abstraction would need to re-derive the exact resolve/reject wiring in add(), since callers depend on it returning a real Promise tied 1:1 to a queued node.

Tech Stack TypeScript authored, built via esbuild through a custom build.ts script producing dual ESM+CJS output plus .d.ts declarations generated separately via tsc --emitDeclarationOnly. No runtime dependencies at all — devDependencies are limited to benchmarking comparisons (async, fastq, p-limit, promise-queue, queue), mitata for benchmarks, and typescript/@types/bun. Tests run under Bun’s built-in test runner (bun:test) and are mirrored under Deno’s native test runner, executed against both the dev TypeScript source and the built dist output (DIST=true) to catch build regressions. Distributed via npm and JSR (with a JSR score badge in the README) alongside a deno.json manifest for direct Deno consumption. No CI workflow was found in the repo (no .github/workflows), so testing and publishing appear to be run locally by the maintainer.

Code Quality Tests exist under test/bun.test.ts (mirrored for Deno, plus a dedicated test/rl.test.ts for the rate-limited variant), covering concurrency behavior, rejection handling, size()/active() accounting, all() batch behavior, and FIFO ordering guarantees — a genuinely solid suite for the library’s surface area, run against both source and build output. No dedicated linter/formatter config was found beyond deno.json’s narrow lint.include; style consistency across the small codebase looks hand-maintained rather than tool-enforced. Error handling is minimal by design: rejections propagate through native Promise semantics, with the queue simply forwarding a wrapped function’s own rejection to the caller’s promise rather than swallowing it. Typing is strict TypeScript throughout (generic add<T>, all<T>), with .d.ts generation as a separate build step. No CI pipeline was found, so tests — while present and meaningful — aren’t automatically enforced on push or PR.

API Design The public API is deliberately minimal and ergonomic: a single newQueue(concurrency) factory returns an object with just six methods (add, all, done, clear, active, size), and queue.add() returns a real Promise<T> that resolves or rejects exactly like the wrapped function would on its own, so callers don’t need queue-specific error handling. queue.all() mirrors Promise.all closely enough (accepting a mix of bare promises and promise-returning functions) that switching from Promise.all requires almost no code changes. Getting started needs no configuration beyond picking a concurrency number, and the rate-limited variant only adds two optional parameters (rate, interval) to the same factory rather than introducing a new API shape. Documentation leans entirely on the README (usage snippets, an interface listing, and extensive benchmark tables) with full JSDoc on exported types and functions, but there’s no example directory or hosted docs site beyond the JSR score badge, and no CONTRIBUTING guide for prospective contributors.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search