Location
core/src/utils/throttler.ts:23
Code
async throttle(cost: number = 1): Promise<void> {
return new Promise<void>((resolve) => {
this.queue.push({ resolve, cost }); // no depth check
if (!this.running) {
this.running = true;
this.loop();
}
});
}
Growth Pattern
Every HTTP request made through BaseExchange flows through this throttler (BaseExchange.ts:436). When requests arrive faster than the configured refillRate allows, they are enqueued with no upper bound. Each queued item holds a live Promise resolver closure (~800 bytes of V8 heap per entry). Under a thundering-herd scenario — e.g., fetchMarkets() called across many exchanges simultaneously, or a client that fires hundreds of watchOrderBook loops — the queue inflates without any backpressure signal to the caller.
The default rateLimit is 1000 ms (1 req/sec). At 500 concurrent callers each making one HTTP call, 499 queue up immediately and stay queued. There is no maximum queue depth, no rejection, and no timeout on queued items.
OOM Estimate
- Queue item: ~64 bytes (object with two pointers) + ~800 bytes Promise closure = ~864 bytes/item
- At 10,000 queued items: ~8.6 MB
- Under sustained burst (100 req/s into a 1 req/s throttle): queue grows at 99 items/sec
- After 10 minutes: ~59,400 items ≈ 51 MB of live closures, none collectable until resolved
- Servers with 512 MB heap running several parallel exchange clients could OOM in under 1 hour
Suggested Fix
Add a maxQueueDepth constructor option (e.g., default 512). In throttle(), if this.queue.length >= maxQueueDepth, reject immediately with a RateLimitExceeded error rather than enqueuing.
Found by automated unbounded operations audit
Location
core/src/utils/throttler.ts:23Code
Growth Pattern
Every HTTP request made through
BaseExchangeflows through this throttler (BaseExchange.ts:436). When requests arrive faster than the configuredrefillRateallows, they are enqueued with no upper bound. Each queued item holds a live Promise resolver closure (~800 bytes of V8 heap per entry). Under a thundering-herd scenario — e.g.,fetchMarkets()called across many exchanges simultaneously, or a client that fires hundreds ofwatchOrderBookloops — the queue inflates without any backpressure signal to the caller.The default
rateLimitis 1000 ms (1 req/sec). At 500 concurrent callers each making one HTTP call, 499 queue up immediately and stay queued. There is no maximum queue depth, no rejection, and no timeout on queued items.OOM Estimate
Suggested Fix
Add a
maxQueueDepthconstructor option (e.g., default 512). Inthrottle(), ifthis.queue.length >= maxQueueDepth, reject immediately with aRateLimitExceedederror rather than enqueuing.Found by automated unbounded operations audit