A zero-runtime-dependency concurrency limiter for Node.js and browsers.
It supports FIFO queueing, AbortSignal-based cancellation and wait timeouts (queueing and onIdle()), and idle waiting via onIdle().
Runs on Node.js β₯18 and modern browsers.
Used in production by Pastellink, a Discord bot trusted by 2,500+ servers.
π Other languages:
npm i @selentia/async-limiterimport { createLimit } from '@selentia/async-limiter';
const limit = createLimit(5);
await Promise.all([
limit(() => fetch('/a')),
limit(() => fetch('/b')),
]);import { Limiter } from '@selentia/async-limiter';
const limiter = new Limiter(3, {
maxQueue: 100,
queueTimeoutMs: 2000,
});
await limiter.run(async () => {
// ...
});
await limiter.onIdle();createLimit() returns a function with the same call signature as Limiter#run(),
plus observability helpers:
limit.activeCount; // number of running tasks
limit.pendingCount; // number of queued tasks
limit.limiter; // underlying Limiter instance
limit.onIdle(); // wait until idleimport type { LimitFn } from '@selentia/async-limiter';
type LimitFn = {
<T>(task: () => T | Promise<T>, options?: RunOptions): Promise<T>;
readonly activeCount: number;
readonly pendingCount: number;
onIdle(options?: IdleOptions): Promise<void>;
readonly limiter: Limiter;
};Creates a limiter with a fixed concurrency limit.
Runs task under the concurrency limit.
Resolves when the limiter becomes idle:
activeCount === 0 && pendingCount === 0| Option | Type | Default | Description |
|---|---|---|---|
maxQueue |
number |
Infinity |
Maximum number of queued tasks (running tasks are not included). |
queueTimeoutMs |
number |
undefined |
Time limit (ms) while waiting in the queue. |
signal |
AbortSignal |
undefined |
Default abort signal applied while waiting (can be overridden per call). |
| Option | Type | Default | Description |
|---|---|---|---|
signal |
AbortSignal |
undefined |
Per-call abort signal (overrides LimiterOptions.signal). |
queueTimeoutMs |
number |
undefined |
Per-call queue wait timeout (overrides LimiterOptions.queueTimeoutMs). |
| Option | Type | Default | Description |
|---|---|---|---|
signal |
AbortSignal |
undefined |
Abort waiting for idle. |
timeoutMs |
number |
undefined |
Time limit (ms) for waiting until idle. |
signalandqueueTimeoutMsapply only while waiting (i.e., before a task starts running).- Once a task starts running, it cannot be cancelled by the limiter.
- Aborted / timed-out entries are removed and cleaned up, and they never block the queue.
onIdle({ signal, timeoutMs })supports both abort and timeout while waiting for idle.
Errors can be handled via instanceof or error.code.
| Error | Code | When it occurs |
|---|---|---|
QueueOverflowError |
ERR_ASYNC_LIMITER_QUEUE_OVERFLOW |
The queue is full (pendingCount >= maxQueue). |
QueueTimeoutError |
ERR_ASYNC_LIMITER_QUEUE_TIMEOUT |
A task waited too long in the queue before starting. |
AbortError |
ERR_ASYNC_LIMITER_ABORTED |
Aborted while waiting (queue wait or idle wait). |
IdleTimeoutError |
ERR_ASYNC_LIMITER_IDLE_TIMEOUT |
The limiter did not become idle within timeoutMs. |
Example:
import { Limiter, QueueTimeoutError } from '@selentia/async-limiter';
const limiter = new Limiter(3);
try {
await limiter.run(task, { queueTimeoutMs: 300 });
} catch (err) {
if (err instanceof QueueTimeoutError) {
// queued for too long
}
}- Queued tasks start in FIFO order.
(Running tasks may complete in any order.) - The concurrency limit is never exceeded.
- Aborted/timed-out tasks are fully cleaned up and never become βzombiesβ.
onIdle()resolves only when:
activeCount === 0 && pendingCount === 0MIT