Skip to content

Unbounded: core/src/exchanges/polymarket/websocket.ts — userCallbacks array grows without deduplication or cap #380

@realfishsam

Description

@realfishsam

Location

core/src/exchanges/polymarket/websocket.ts:199

Code

private userCallbacks: UserChannelCallback[] = [];

async watchUserFills(conditionIds: string[], callback: UserChannelCallback): Promise<void> {
    // ...
    this.userCallbacks.push(callback);  // ← no dedup, no max size
    this.userConditionIds = [...new Set([...this.userConditionIds, ...conditionIds])];

    if (this.userWs) {
        // Already connected — re-subscribe
        this.sendUserSubscription(creds);
        return;
    }
    await this.connectUserChannel(creds);
}

On every incoming user-channel message, all callbacks are invoked:

for (const cb of this.userCallbacks) {
    try { cb(event); } catch (e) { ... }
}

Growth Pattern

watchUserFills() appends the caller's callback to userCallbacks on every invocation without deduplication. The only removal path is unwatchUserFills(), which replaces the entire array with []. If a caller:

  • registers multiple callbacks for different condition sets (e.g., one callback per market)
  • calls watchUserFills inside a reconnect or retry loop
  • holds a reference to a PolymarketExchange instance that reconnects internally

…then userCallbacks grows linearly with each call. On every last_trade_price-equivalent user event, the inner for (const cb of this.userCallbacks) loop iterates all accumulated callbacks, many of which may be duplicates doing redundant work.

OOM Estimate

  • Each callback closure: ~1–4 KB depending on captured scope
  • At 1 watchUserFills call per minute over an 8-hour trading session: 480 callbacks × 4 KB = ~2 MB
  • If a reconnect loop re-calls watchUserFills at 1 Hz: 3,600 callbacks/hour × 4 KB = ~14 MB/hour
  • CPU impact: O(n) callback dispatch on every user event; at 100 trades/min with 3,600 callbacks: 360,000 callback invocations/min
  • Not an immediate OOM risk but causes quadratic CPU growth and memory creep over long sessions

Suggested Fix

Deduplicate callbacks by identity before pushing: if (!this.userCallbacks.includes(callback)) this.userCallbacks.push(callback). Alternatively, use a Set<UserChannelCallback> instead of an array to prevent duplicates automatically. Add a maxCallbacks guard as a safety net.


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