Skip to content

Unbounded: core/src/exchanges/polymarket/websocket.ts — pendingTrades buffer has no eviction #334

@realfishsam

Description

@realfishsam

Location

core/src/exchanges/polymarket/websocket.ts:442-446

Code

private handleTrade(event: any) {
    // ...
    const resolvers = this.tradeResolvers.get(id);
    if (resolvers && resolvers.length > 0) {
        resolvers.forEach((r) => r.resolve([trade]));
        this.tradeResolvers.set(id, []);
    } else {
        if (!this.pendingTrades.has(id)) {
            this.pendingTrades.set(id, []);
        }
        this.pendingTrades.get(id)!.push(trade);  // ← unbounded
    }
}

Growth Pattern

handleTrade is called on every last_trade_price WebSocket message from Polymarket. When no watchTrades() caller is awaiting (i.e., tradeResolvers for that asset is empty), the trade is buffered in pendingTrades. This buffer is only drained when the next watchTrades() call comes in (line 147–149). If a caller subscribes to market data via subscribe() but then stops calling watchTrades() (e.g., their streaming loop is paused, or they only called watchOrderBook), the per-asset trade buffer grows without bound. There is no max size, no TTL, and no eviction.

OOM Estimate

  • Each Trade object: ~200 bytes
  • Polymarket active markets: ~100–500 assets subscribed simultaneously is realistic
  • Trade frequency: 5–50 events/sec per active market during peak hours
  • Conservative: 50 assets × 5 trades/sec = 250 trades/sec buffered when no watcher is active
  • After 30 minutes: 450,000 Trade objects ≈ 90 MB
  • After 2 hours at peak (50 TPS × 200 assets): ~720 MB → OOM on default Node heap

Suggested Fix

Implement a ring buffer per asset: cap pendingTrades.get(id) at e.g. 1000 entries; when the cap is reached, drop the oldest trade (shift()). Alternatively, expose a maxPendingTrades config option on PolymarketWebSocketConfig.


Found by automated unbounded operations audit

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions