mrkeyoor.com_
Tue 22 Sept 22:32 UTC
npmUtilsupdated 21 Sept 2026

tiny-async-pool review

tiny-async-pool 2.1.0 applies one async or promise-returning function to a synchronous iterable while capping the number of active calls. It is an async generator, so consumers receive values when tasks finish rather than in input order. The first observed rejection throws out of iteration. Version 2 replaced the 1.x all-results promise with that streaming contract; 2.1.0 tightened one-at-a-time `Promise.race` consumption and covers workers that return plain values. Our browser bundle was 0.8 KB minified and 0.5 KB gzipped. The README now recommends Node 24's native `Readable.map` instead when available.

Verdict

tiny-async-pool 2.1.0 added only 0.5 KB gzipped and zero audit findings in our install, while yielding results in completion order and leaving failed-task siblings running. Use it for that exact small pre-Node-24 loop; choose native `Readable.map`, p-map, or p-queue when ordering, cancellation, or scheduling policy matters.

We installed it

Lab card: what happened when we installed tiny-async-poolScreenshot of tiny-async-pool documentation
Install✓ · 0.3s1 package on disk · 1 MB
ImportESM import works · require() works · CommonJS package
Browser0.5 KBgzipped (0.8 KB minified), bundled with esbuild
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does tiny-async-pool install cleanly?

Yes. In a fresh container with an empty cache, npm install tiny-async-pool finished in 0.3s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does tiny-async-pool add to a browser bundle?

0.5 KB gzipped (0.8 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does tiny-async-pool work with both ESM and CommonJS?

Yes. Both import 'tiny-async-pool' and require('tiny-async-pool') worked in Node 22 in our run. The package is published as CommonJS.

Does tiny-async-pool include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

tiny-async-pool or p-limit: which should you use?

p-limit: Choose it for a reusable concurrency gate with active and pending counts around separately scheduled calls. tiny-async-pool 2.1.0 added only 0.5 KB gzipped and zero audit findings in our install, while yielding results in completion order and leaving failed-task siblings running.

When should you not use tiny-async-pool?

Node 24 or newer is the only target. The package README directs those users to Readable.from(items).map(worker, { concurrency }).

API stability4/5The 2.x API has one default function with 3 positional inputs and returns one async iterator. Version 2.1.0 has stayed published since May 2022, and its entire runtime fits in a short source file. The major-version history warrants care: 1.x resolved a promise after all tasks, whereas 2.x emits values as they complete. Missing declarations and an exports map leave module and type compatibility outside a checked contract.
Docs4/5The README animates a 2-worker timeline, states completion-order yielding and fail-fast rejection, lists accepted synchronous iterable types, explains both worker arguments, supplies a 1.x-style collection wrapper, and points Node 24 users to a native API. It omits several source-visible behaviors: lazy startup, absence of concurrency validation, continued sibling work after rejection, and scheduling delays caused by a slow consumer.
Maintenance3/5npm published 2.1.0 on May 10, 2022, while GitHub shows a later push on July 29, 2025. The repository is not archived and currently combines 2 open issues and pull requests. Updating the README to recommend Node 24's native stream mapper is useful maintenance. No release has added types, validation, cancellation, async-iterable input, or modern exports since 2022, which is acceptable for frozen code but limits new integrations.
Ecosystem4/5The latest completed npm week recorded 5,065,811 downloads, and GitHub reports 822 stars. Standard promises and synchronous iterables let it work with arrays, sets, maps, typed arrays, strings, and generators without adapters. Its zero-dependency graph and 0.5 KB measured gzip cost fit utility bundles. Adoption friction remains for TypeScript, ESM-only projects, async producers, and systems that need queue telemetry or cancellation.

Use it if

  • A finite array, Set, Map, typed array, string, or synchronous generator needs a small fixed in-flight limit.
  • The caller can process outputs as soon as each task finishes and does not require input ordering.
  • Failing the iteration on the first worker error matches the batch's transaction or reporting rules.
  • A dependency-free 0.5 KB gzip helper is preferable to a queue package with priorities and lifecycle state.
Skip it if

Setup reality

We installed tiny-async-pool 2.1.0 in 0.3 seconds. It left one package using 1 MB on disk; its unpacked files total 24 KB. There are zero direct dependencies and zero peer dependencies, and npm audit found zero known vulnerabilities. The package is CommonJS with no exports map. Both require() and ESM import worked in Node 22, but no TypeScript declarations were included.

Calling asyncPool creates an async generator; work starts when the consumer requests its first value. Pass a positive integer concurrency, a synchronous iterable, and a worker. The README states >= 1, yet the 2.1.0 source performs no validation. Check zero, negatives, fractions, and NaN at configuration boundaries. The worker's second argument is the original iterable, not an item index. Add indexes to the input if identity or ordering must survive completion-order output.

The pool stores active promises in a Set and waits on Promise.race at the limit. A rejection escapes immediately, but sibling calls already running receive no stop signal. Put AbortSignal, timeouts, and cleanup inside the worker when external operations must end. Catch per-item errors inside that worker and return tagged results if partial success is allowed. Slow processing in the for await body also pauses the generator before it can pull and schedule more input.

Our esbuild result was 0.8 KB minified and 0.5 KB gzipped, so weight is almost irrelevant. Semantics decide the install. Version 1 collected everything; version 2.1.0 streams completion order. It limits simultaneous promises, not requests per second or remote quota. On Node 24, the README recommends the stable stream mapping API. Elsewhere, p-map fits ordered mapping, p-limit fits a reusable gate, and p-queue fits scheduling that needs state or priorities.

Patterns

Run at most 4 workers limit-concurrency

import asyncPool from 'tiny-async-pool'

for await (const profile of asyncPool(4, userIds, async (id) => {
  const response = await fetch(`/api/users/${id}`)
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
})) {
  save(profile)
}

At most 4 calls run together, and profiles arrive in completion order rather than `userIds` order.

Build an array from the async iterator collect-results

async function collectPool(limit, items, worker) {
  const values = []
  for await (const value of asyncPool(limit, items, worker)) {
    values.push(value)
  }
  return values
}

const values = await collectPool(3, items, transform)

The collected array still follows 2.1.0 completion order; this wrapper does not recreate the ordered 1.x result.

Sort tagged results back into input order restore-input-order

const tagged = items.map((item, index) => ({ item, index }))
const done = []
for await (const value of asyncPool(4, tagged, async ({ item, index }) => ({
  index,
  output: await transform(item),
}))) {
  done.push(value)
}
const ordered = done.sort((a, b) => a.index - b.index).map((x) => x.output)

Ordering forces the caller to retain all results and wait for completion; p-map is simpler when this is the default requirement.

Handle the first rejected worker catch-pool-error

try {
  for await (const result of asyncPool(5, items, worker)) {
    consume(result)
  }
} catch (error) {
  console.error('pool stopped yielding', error)
}

The iteration throws, but 2.1.0 does not cancel promises that were already added to its active Set.

Return per-item outcomes continue-after-errors

for await (const outcome of asyncPool(4, items, async (item) => {
  try {
    return { ok: true, item, value: await worker(item) }
  } catch (error) {
    return { ok: false, item, error }
  }
})) {
  if (!outcome.ok) report(outcome)
}

Catching inside the worker prevents fail-fast behavior; only use partial success when downstream state can tolerate it.

Bound each fetch separately timeout-worker

for await (const response of asyncPool(4, urls, (url) => {
  return fetch(url, { signal: AbortSignal.timeout(10_000) })
})) {
  console.log(response.status)
}

The 10-second signal belongs to each fetch. tiny-async-pool supplies no shared timeout or cancellation controller.

Reject an invalid concurrency setting validate-limit

function checkedPool(limit, items, worker) {
  if (!Number.isInteger(limit) || limit < 1) {
    throw new RangeError('limit must be a positive integer')
  }
  return asyncPool(limit, items, worker)
}

Version 2.1.0 documents a minimum of 1 but does not enforce it in source.

Consume key-value pairs directly process-map

const jobs = new Map([['invoice', a], ['receipt', b]])
for await (const [name, output] of asyncPool(2, jobs, async ([name, input]) => {
  return [name, await render(input)]
})) {
  console.log(name, output)
}

Map yields `[key, value]` pairs; the worker's second parameter would be the original Map, not a numeric index.

Pull from a finite synchronous generator process-generator

function* pages(total) {
  for (let page = 1; page <= total; page += 1) yield page
}

for await (const data of asyncPool(3, pages(20), fetchPage)) {
  store(data)
}

Synchronous generators work in 2.1.0. Async generators do not because input is consumed with `for...of`.

Use Node 24's native mapper use-node-stream-map

import { Readable } from 'node:stream'

for await (const result of Readable.from(items).map(worker, { concurrency: 4 })) {
  consume(result)
}

The project README recommends this path on Node 24 and newer, removing the package dependency.

Alternatives

PackageRegistryPick it when
p-limitnpmChoose it for a reusable concurrency gate with active and pending counts around separately scheduled calls.
p-mapnpmChoose it for typed concurrent mapping when input order and richer error controls matter.
p-queuenpmChoose it when priorities, pause and resume, interval limits, and queue observability are required.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.