{"_id":"@cajax/axios-parallel-limit","_rev":"4-ee6bd7279c3a1f3859e6dc8c40a02066","name":"@cajax/axios-parallel-limit","dist-tags":{"latest":"1.1.1"},"versions":{"1.0.0":{"name":"@cajax/axios-parallel-limit","version":"1.0.0","keywords":["axios","parallel","limit","request","http","https","rest","api"],"author":{"name":"cajax"},"license":"MIT","_id":"@cajax/axios-parallel-limit@1.0.0","maintainers":[{"name":"cajax","email":"cajax1@gmail.com"}],"homepage":"https://github.com/cajax/axios-parallel-limit","bugs":{"url":"https://github.com/cajax/axios-parallel-limit/issues"},"dist":{"shasum":"c2d59392d640a5ce047a2443c0c80970f72fd842","tarball":"https://registry.npmjs.org/@cajax/axios-parallel-limit/-/axios-parallel-limit-1.0.0.tgz","fileCount":6,"integrity":"sha512-BtFL+LrwEsSW/sXIO1tY5nbPRfw4Nm28OOW9MKheK7br0mWDXL3nx7iKOj9IuVgJzKY21C7M1ovvIDGxyWPDyQ==","signatures":[{"sig":"MEUCIQDNU9uRaltpMESoXLmc3l8TNhcgmdKjYk4yaJA1+fyf4AIga069kc3lbwQ5qSzjZWr6gducqCLK2fB69NRvKcyHzIo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":11369},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"gitHead":"30a8369d418856bb18834897eed30c06322fa30f","scripts":{"test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","build":"tsc"},"_npmUser":{"name":"cajax","email":"cajax1@gmail.com"},"repository":{"url":"git+https://github.com/cajax/axios-parallel-limit.git","type":"git"},"_npmVersion":"9.2.0","description":"Limit parallel requests in Axios","directories":{"test":"test","example":"examples"},"_nodeVersion":"18.19.1","dependencies":{"axios":"^1.13.2","p-limit":"^6.2.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.2.0","ts-jest":"^29.4.5","typescript":"^5.9.3","@types/jest":"^30.0.0","@types/node":"^24.10.1","axios-mock-adapter":"^2.1.0"},"_npmOperationalInternal":{"tmp":"tmp/axios-parallel-limit_1.0.0_1763997498144_0.17777587828303254","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@cajax/axios-parallel-limit","version":"1.0.1","keywords":["axios","parallel","limit","request","http","https","rest","api"],"author":{"name":"cajax"},"license":"MIT","_id":"@cajax/axios-parallel-limit@1.0.1","maintainers":[{"name":"cajax","email":"cajax1@gmail.com"}],"homepage":"https://github.com/cajax/axios-parallel-limit","bugs":{"url":"https://github.com/cajax/axios-parallel-limit/issues"},"dist":{"shasum":"94c85c1afc1f4943095a97ed3e25402e58e789d1","tarball":"https://registry.npmjs.org/@cajax/axios-parallel-limit/-/axios-parallel-limit-1.0.1.tgz","fileCount":9,"integrity":"sha512-FtsAMGYdLMgdx+lJQ8pRgq/gqp2PrR/Oe3CothwEKKYXLjbaI6yPyhVW7IVxFpNB4sxrCtYKzxYUZVZDYc5i/w==","signatures":[{"sig":"MEQCIByIToNo+XBox7RFGaMNDKGtbhKFBtZp7WBvyzab0XgHAiB4+1X4Svh8ktloRk/WRxOWoxCt8Gd2PMObmW/AfMXyFA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":48401},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"82988bc0b5a9baf16f02ec6bf22869b61fea9df8","scripts":{"test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","build":"tsup"},"_npmUser":{"name":"cajax","email":"cajax1@gmail.com"},"repository":{"url":"git+https://github.com/cajax/axios-parallel-limit.git","type":"git"},"_npmVersion":"9.2.0","description":"Limit parallel requests in Axios","directories":{"test":"test","example":"examples"},"_nodeVersion":"18.19.1","dependencies":{"axios":"^1.13.2"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.2.0","tsup":"^8.5.1","p-limit":"^7.2.0","ts-jest":"^29.4.5","typescript":"^5.9.3","@types/jest":"^30.0.0","@types/node":"^24.10.1","axios-mock-adapter":"^2.1.0"},"_npmOperationalInternal":{"tmp":"tmp/axios-parallel-limit_1.0.1_1764067139874_0.03899823843001737","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@cajax/axios-parallel-limit","version":"1.1.0","keywords":["axios","parallel","limit","request","http","https","rest","api"],"author":{"name":"cajax"},"license":"MIT","_id":"@cajax/axios-parallel-limit@1.1.0","maintainers":[{"name":"cajax","email":"cajax1@gmail.com"}],"homepage":"https://github.com/cajax/axios-parallel-limit","bugs":{"url":"https://github.com/cajax/axios-parallel-limit/issues"},"dist":{"shasum":"249fb3afb4ecb78aa748ede70a9257d4f7cfd34d","tarball":"https://registry.npmjs.org/@cajax/axios-parallel-limit/-/axios-parallel-limit-1.1.0.tgz","fileCount":9,"integrity":"sha512-AQyT8HzR58Pv1vpq9oav7u1Lv/zFxIjJ/nUjkjV+eNKVvPiTHGreTAjCtK3Rsr1G1vQ/FAyn9/HvLdIa4+CpLw==","signatures":[{"sig":"MEUCICKDRD103+lRAEFT9T+TRg+sc2gQ3BZQvWznWkvwjcy6AiEAqK3sk5p8r1dRydj2CGJPF5JKuwvEmN6JDrVugJd7Bmg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":91621},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"5c590082675538ce396be68de714a96f9febd207","scripts":{"test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","build":"tsup"},"_npmUser":{"name":"cajax","email":"cajax1@gmail.com"},"repository":{"url":"git+https://github.com/cajax/axios-parallel-limit.git","type":"git"},"_npmVersion":"10.8.2","description":"Limit parallel requests in Axios","directories":{"test":"test","example":"examples"},"_nodeVersion":"20.20.2","dependencies":{"axios":"^1.13.2"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.2.0","tsup":"^8.5.1","ts-jest":"^29.4.5","typescript":"^5.9.3","@types/jest":"^30.0.0","@types/node":"^24.10.1","axios-mock-adapter":"^2.1.0"},"_npmOperationalInternal":{"tmp":"tmp/axios-parallel-limit_1.1.0_1780346002883_0.7966038812416583","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@cajax/axios-parallel-limit","version":"1.1.1","description":"Limit parallel requests in Axios","type":"module","main":"dist/index.cjs","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","build":"tsup"},"keywords":["axios","parallel","limit","request","http","https","rest","api"],"author":{"name":"cajax"},"bugs":{"url":"https://github.com/cajax/axios-parallel-limit/issues"},"repository":{"type":"git","url":"git+https://github.com/cajax/axios-parallel-limit.git"},"homepage":"https://github.com/cajax/axios-parallel-limit","license":"MIT","dependencies":{"axios":"^1.13.2"},"devDependencies":{"@types/jest":"^30.0.0","@types/node":"^24.10.1","axios-mock-adapter":"^2.1.0","jest":"^30.2.0","ts-jest":"^29.4.5","tsup":"^8.5.1","typescript":"^5.9.3"},"directories":{"example":"examples","test":"test"},"_id":"@cajax/axios-parallel-limit@1.1.1","gitHead":"77324d852ca6211726e0804f83e4c79158eb939f","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-0xzIPli/GNLdd0WEf2m8/UI8mcbRkbNccZEw9M7wDZOR93NQHZIsjj/bLb7NBGh+r/lgF37EP55sQyTrJ/86ow==","shasum":"c1adc53afee985e358c4d5116fc59dd238855013","tarball":"https://registry.npmjs.org/@cajax/axios-parallel-limit/-/axios-parallel-limit-1.1.1.tgz","fileCount":9,"unpackedSize":108821,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDIQkFaIMMDSgvb7icSjZaDQcw2n3R3dvvJI6TiV8dfDwIhAPYZqP0jmqNVC3Cext+7UYRywmlY1Z0hnjTFsFgsG9GM"}]},"_npmUser":{"name":"cajax","email":"cajax1@gmail.com"},"maintainers":[{"name":"cajax","email":"cajax1@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/axios-parallel-limit_1.1.1_1780414663630_0.9355069289964604"},"_hasShrinkwrap":false}},"time":{"created":"2025-11-24T15:18:18.094Z","modified":"2026-06-02T15:37:43.894Z","1.0.0":"2025-11-24T15:18:18.319Z","1.0.1":"2025-11-25T10:39:00.052Z","1.1.0":"2026-06-01T20:33:23.062Z","1.1.1":"2026-06-02T15:37:43.799Z"},"bugs":{"url":"https://github.com/cajax/axios-parallel-limit/issues"},"author":{"name":"cajax"},"license":"MIT","homepage":"https://github.com/cajax/axios-parallel-limit","keywords":["axios","parallel","limit","request","http","https","rest","api"],"repository":{"type":"git","url":"git+https://github.com/cajax/axios-parallel-limit.git"},"description":"Limit parallel requests in Axios","maintainers":[{"name":"cajax","email":"cajax1@gmail.com"}],"readme":"# axios-parallel-limit\n\nA lightweight Axios wrapper that caps the number of in-flight requests and queues the rest. It lets you control the concurrency of your HTTP requests so your application doesn't overwhelm a downstream service or the client itself.\n\nOn top of the concurrency cap it adds an **optional bounded queue** (`maxQueueSize`) and an **optional queue-wait deadline** (`queueTimeout`) so that, under overload, requests **fail fast** with a typed error instead of piling up behind an unbounded queue and hanging until some far-away timeout kills them. This is the classic bounded-work-queue / bulkhead pattern used by thread-pool executors and resilience libraries.\n\nAll of the new behavior is **opt-in** — with only `maxRequests` set, the wrapper behaves exactly as before.\n\n## Installation\n\n```bash\nnpm install @cajax/axios-parallel-limit\n```\n\n## Usage\n\n```typescript\nimport axios from 'axios';\nimport { axiosParallelLimit } from 'axios-parallel-limit';\n\n// Create an Axios instance\nconst http = axios.create({\n  baseURL: 'https://api.example.com'\n});\n\n// Apply the parallel limit\naxiosParallelLimit(http, {\n  maxRequests: 5, // Limit to 5 concurrent requests\n  onActiveCountChange: (active) => {\n    console.log(`Active requests: ${active}`);\n  },\n  onPendingCountChange: (pending) => {\n    console.log(`Pending requests: ${pending}`);\n  }\n});\n\n// Now use the axios instance as usual\n// Only 5 requests will run in parallel, others will be queued\nfor (let i = 0; i < 20; i++) {\n  http.get(`/items/${i}`).then(response => {\n    console.log(`Item ${i} loaded`);\n  });\n}\n```\n\n## Configuration\n\nThe `axiosParallelLimit` function takes two arguments:\n1. `axiosInstance`: The Axios instance to wrap.\n2. `options`: An object with the following properties:\n\n| Option | Type | Required | Default | Description |\n|--------|------|----------|---------|-------------|\n| `maxRequests` | `number` | Yes | — | Maximum number of requests that can run simultaneously. |\n| `onActiveCountChange` | `(count: number) => void` | No | — | Called when the number of **active** (in-flight) requests changes. |\n| `onPendingCountChange` | `(count: number) => void` | No | — | Called when the number of **pending** (queued) requests changes. |\n| `queueTimeout` | `number` (ms) | No | disabled | Max time a request may spend **waiting in the queue** for a free slot. See [Bounded queue](#bounded-queue--back-pressure). |\n| `maxQueueSize` | `number` | No | unbounded | Hard upper bound on queue depth. Requests beyond it are rejected immediately (load shedding). |\n| `onDispatch` | `(info: QueueEventInfo) => void` | No | — | Called when a request starts executing, reporting its **queue-wait latency** (`waitMs`). |\n| `onQueueTimeout` | `(info: QueueEventInfo) => void` | No | — | Called when a request is rejected due to `queueTimeout`. |\n| `onQueueOverflow` | `(info: QueueEventInfo) => void` | No | — | Called when a request is rejected due to `maxQueueSize`. |\n\n`QueueEventInfo` is `{ config, waitMs, queueSize }` — the originating request `config`, the milliseconds spent waiting in the queue (`0` for an immediate dispatch or an overflow), and the number of requests still queued when the event fired.\n\n## Bounded queue & back-pressure\n\n### Why\n\nCapping concurrency alone still uses an **unbounded** queue: every request above the cap waits, with no limit on how long or how deep. With `N` slots and per-request service time `S`, a request queued at position `P` waits roughly `(P / N) * S` before it even *starts*. If the downstream slows down (`S` rises) or arrivals outpace the drain rate, the queue — and the wait — grow without bound. A caller can then wait far longer than its own deadline, and longer than any outer HTTP-server / proxy / load-balancer timeout sitting above it. When that outer timeout fires, the request is killed with no useful error: it was queued, never dispatched. There's also no back-pressure signal — nothing fails fast, so load keeps piling onto a queue that can't drain.\n\n`queueTimeout` and `maxQueueSize` are the two levers that fix this.\n\n### `queueTimeout` — bound how long a request waits\n\n`queueTimeout` (milliseconds) is the maximum time a request may spend **waiting in the queue** for a free slot. If it isn't dispatched within that window it is removed from the queue (**never executed**) and its promise rejects with a [`QueueTimeoutError`](#errors--type-guards).\n\n- The timer starts the instant the request is deferred (no slot available) and is cleared the instant it is dispatched.\n- It measures **queue-wait only** — never the execution/HTTP time.\n- A request that gets a slot immediately is **never** subject to it.\n\n**It composes with — does not replace — Axios's own request `timeout`.** Axios's `timeout` bounds the HTTP exchange once the request is in flight; `queueTimeout` bounds the wait *before* it goes in flight. A request's worst-case total budget is therefore:\n\n```\nworst-case total ≈ queueTimeout (queue-wait) + timeout (execution)\n```\n\nSize them so that sum stays comfortably under any outer deadline (server socket timeout, proxy, load balancer) — that way the request fails *here*, fast, with a typed error, instead of being killed opaquely from far away.\n\n### `maxQueueSize` — bound how deep the queue gets\n\n`maxQueueSize` is a hard limit on the number of waiting requests. When the queue is full, new requests are rejected **immediately** with a [`QueueFullError`](#errors--type-guards) instead of being enqueued (load shedding / fail-fast back-pressure). Requests already in the queue are unaffected.\n\n### Per-request `queueTimeout` override\n\nA single call can override the instance-level `queueTimeout` via a `queueTimeout` field on its request config (typed via module augmentation):\n\n```typescript\n// This call may wait at most 250ms in the queue, regardless of the instance default.\nhttp.get('/report', { queueTimeout: 250 });\n```\n\nWhen omitted, the instance-level `queueTimeout` (if any) applies.\n\n### Cancellation while queued\n\nIf a request carries an `AbortSignal` (`config.signal`) or an Axios cancel token (`config.cancelToken`) and it is aborted **while still waiting in the queue**, it is removed from the queue immediately, its queue-timeout timer is cleared, and its promise rejects with the standard Axios cancellation error (so `axios.isCancel(err)` is `true`). Work is never dispatched for an already-cancelled caller.\n\n```typescript\nconst controller = new AbortController();\nconst p = http.get('/slow', { signal: controller.signal });\ncontroller.abort(); // if still queued, it leaves the queue and never executes\n```\n\n### Example: a client calling a slower downstream\n\n```typescript\nimport axios from 'axios';\nimport { axiosParallelLimit, isQueueTimeoutError, isQueueFullError } from 'axios-parallel-limit';\n\nconst downstream = axios.create({\n  baseURL: 'https://downstream.internal',\n  timeout: 10_000,          // execution timeout: bound the HTTP exchange itself\n});\n\naxiosParallelLimit(downstream, {\n  maxRequests: 10,          // match the downstream's safe concurrency\n  maxQueueSize: 100,        // ~10x the cap: absorb bursts, then shed load\n  queueTimeout: 5_000,      // wait at most 5s for a slot, then fail fast\n  onQueueTimeout: ({ waitMs, config }) =>\n    console.warn(`shed (queue-wait ${waitMs}ms): ${config.url}`),\n  onQueueOverflow: ({ config }) =>\n    console.warn(`shed (queue full): ${config.url}`),\n});\n\ntry {\n  const res = await downstream.get('/things');\n  // ...\n} catch (err) {\n  if (isQueueTimeoutError(err)) {\n    // waited too long for a slot — back-pressure, retry later / degrade\n  } else if (isQueueFullError(err)) {\n    // queue is full — shed this request\n  } else {\n    // a normal network/HTTP error (axios.isAxiosError(err)) or a cancellation\n  }\n}\n```\n\n**Recommended starting values:** set `maxRequests` to the concurrency the downstream can comfortably sustain; set `maxQueueSize` to a small multiple of `maxRequests` (e.g. 5–10×) to absorb bursts while bounding worst-case latency and memory; set `queueTimeout` so that `queueTimeout + timeout` stays safely below your outer request deadline. Tune from there using `onDispatch`'s `waitMs` (queue-wait latency) and the active/pending counts.\n\n## Errors & type guards\n\nTwo typed errors are exported so you can distinguish queue rejections from network/HTTP errors:\n\n| Class | `code` | Guard | Thrown when |\n|-------|--------|-------|-------------|\n| `QueueTimeoutError` | `'ERR_QUEUE_TIMEOUT'` | `isQueueTimeoutError(err)` | A request exceeds `queueTimeout` while queued. |\n| `QueueFullError` | `'ERR_QUEUE_FULL'` | `isQueueFullError(err)` | A request is rejected because the queue is full (`maxQueueSize`). |\n\nEach carries a stable, machine-checkable `code`, a descriptive `name`/`message`, and the originating request `config`.\n\n**Design choice (and trade-off):** callers commonly branch on `axios.isAxiosError(err)`. A queue rejection is **not** an Axios error, and these classes are intentionally kept distinct — `axios.isAxiosError(err)` returns `false` for both. Branch on the exported type guards (or the stable `code`) instead. This is the correct, unambiguous choice, but note the trade-off: existing code that only inspects `axios.isAxiosError` / Axios timeout codes (`ECONNABORTED`) will **not** treat a queue rejection as a timeout. (We deliberately do *not* masquerade these as Axios `ECONNABORTED` errors; if you need that, map them yourself in a response interceptor.) A cancellation while queued *does* reject with the standard Axios cancellation error, so `axios.isCancel(err)` works as usual.\n\n## Counts and observability on the new paths\n\nThe two count signals — **active** (in-flight) and **pending** (queued) — stay correct across every exit path. Each transition fires the matching callback exactly once:\n\n| Transition | active | pending | Fires |\n|------------|:------:|:-------:|-------|\n| Admitted immediately (slot free) | `++` | — | `onActiveCountChange`, `onDispatch` (`waitMs: 0`) |\n| Deferred (no slot) | — | `++` | `onPendingCountChange` |\n| Dispatched from the queue | `++` | `--` | `onPendingCountChange`, `onActiveCountChange`, `onDispatch` (`waitMs` = time queued) |\n| Request settles (resolve/reject) | `--` | — | `onActiveCountChange` |\n| **`queueTimeout` fires while queued** | — | `--` | `onPendingCountChange`, `onQueueTimeout` |\n| **Cancelled while queued** | — | `--` | `onPendingCountChange` |\n| **`maxQueueSize` overflow** | — | — | `onQueueOverflow` *(no count callback — the request was never enqueued)* |\n\nKey invariants: `active` never exceeds `maxRequests` on any path; `pending` always equals the number of requests physically in the queue and returns to `0` once it drains, however items left it; and an overflow-rejected request fires **no** count callback (it is rejected before being enqueued).\n\n> Note: callbacks fire on the specific count that changed (e.g. a queue-timeout fires `onPendingCountChange` only — `active` is untouched). This is a small precision improvement over the original, which fired both count callbacks together on every change; the active/pending **values** you observe are unchanged.\n\n## How it works\n\nThe library wraps the Axios adapter to intercept the actual request execution. Each request must acquire one of `maxRequests` concurrency slots before its underlying adapter runs. If a slot is free the request runs immediately; otherwise it waits in an in-memory FIFO queue (subject to `maxQueueSize` and `queueTimeout` when configured) and is dispatched, in order, as slots free up. Timed-out, overflowed, and cancelled requests are removed from the queue and never reach the transport; their queue-timeout timers are always cleared, so nothing leaks after the queue drains.\n\n### Idempotent wrapping (auth / retry interceptors)\n\nWrapping is **idempotent** — a request re-issued by an auth or retry interceptor that reuses the same config object (e.g. an interceptor that refreshes a token on `401` and retries with `instance(error.config)`, or an `axios-retry`-style flow) is **not** double-wrapped. Previously this caused nested slot acquisitions — each retry held an outer slot while waiting for an inner one — that could deadlock the pool once enough retries were in flight. A re-issued request now reuses the single existing adapter wrapper, so it performs exactly one acquisition and stays fully concurrency-limited.\n\n> Apply `axiosParallelLimit` **at most once per axios instance**. Stacking it more than once on the same instance creates independent pools whose wrappers nest by design, which is unsupported and inherently deadlock-prone.\n\n## Migration\n\n`axios-parallel-limit@1.1.0` is a backward-compatible minor release. All new options (`queueTimeout`, `maxQueueSize`, `onDispatch`, `onQueueTimeout`, `onQueueOverflow`) and the per-request `queueTimeout` override are **opt-in**: leave them unset and behavior is identical to before.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}