---
title: "Bounding a Private Account Event Consumer"
description: "Keep private WebSocket handlers short, process account events in order, and make queue overflow or a failed state update visible to the affected consumer."
canonical: "https://siebly.io/blog/private-account-data-streams-in-nodejs-a-practical-websocket-guide"
---

# Bounding a Private Account Event Consumer

Keep private WebSocket handlers short, process account events in order, and make queue overflow or a failed state update visible to the affected consumer.

## Technical Disclaimer

These articles are software engineering references for exchange API integrations. They are not financial, investment, legal, tax, compliance, or trading advice. Use public data, demo, testnet, or paper workflows first. Keep API credentials out of frontend code and disable withdrawal permissions for automation keys.

## Queue Before Doing Slow Work {#build-and-test-a-private-account-event-consumer-step-by-step}

An asynchronous WebSocket listener is not automatically a sequential consumer. If its callback awaits a database operation, the emitter can invoke it again before that operation finishes. Two updates may then write state concurrently even though they arrived in order.

A private consumer needs an explicit processing path. Its connection can remain healthy while that path is falling behind.

<span id="overview"></span><span id="key-takeaways"></span><span id="when-a-node-js-stream-abstraction-is-useful"></span><span id="configure-authentication-without-exposing-secrets"></span><span id="start-with-an-exchange-specific-subscription"></span><span id="bybit-private-topics"></span><span id="choose-the-transport-for-the-job"></span><span id="validate-messages-before-handing-them-downstream"></span><span id="test-behavior-without-assuming-live-market-equivalence"></span><span id="private-subscription-samples"></span><span id="binance-user-data"></span><span id="okx"></span><span id="bitget"></span><span id="kucoin"></span><span id="gate"></span><span id="bitmart"></span><span id="coinbase"></span><span id="kraken"></span><span id="htx"></span><span id="move-from-test-workflow-to-production-deliberately"></span><span id="take-the-next-step-toward-a-testable-consumer"></span><span id="frequently-asked-questions"></span><span id="what-is-a-private-account-data-stream-in-node-js"></span><span id="do-private-account-data-streams-require-api-keys"></span><span id="can-i-test-private-account-streams-without-using-production-credentials"></span>

The following module runs one asynchronous operation at a time. It stops acceptance after overflow or a failed operation. Save it as `serial-consumer.mjs`:

```js title="serial-consumer.mjs"
export function serialConsumer(apply, onFailure, limit = 256) {
  if (!Number.isSafeInteger(limit) || limit < 1) throw new Error('Invalid queue limit');
  const queue = [];
  let running = false;
  let failed = false;
  let active = 0;

  function fail(error) {
    if (failed) return;
    failed = true;
    queue.length = 0;
    onFailure(error);
  }

  async function drain() {
    if (running || failed) return;
    running = true;
    try {
      while (queue.length && !failed) {
        const event = queue.shift();
        active = 1;
        await apply(event);
        active = 0;
      }
    } catch (error) {
      fail(error);
    } finally {
      active = 0;
      running = false;
    }
  }

  return (event) => {
    if (failed) return false;
    if (queue.length + active >= limit) {
      fail(new Error('Private consumer queue overflow'));
      return false;
    }
    queue.push(event);
    void drain();
    return true;
  };
}
```

The limit includes the active operation. This bounds event count, not bytes or processing age. A real consumer should select additional limits according to its payloads and the delay it can tolerate.

<span id="private-account-data-streams-in-nodejs-what-the-term-actually-means"></span><span id="keep-websocket-api-calls-distinct-from-subscriptions"></span><span id="does-a-siebly-sdk-handle-websocket-rate-limits-automatically"></span><span id="how-should-i-recover-if-a-private-websocket-connection-disconnects"></span>

## Attach It to a Defined Topic {#connect-nodejs-consumer-to-private-account-websocket}

For a Bybit consumer, attach an `update` handler before `subscribeV5`. Filter the required private topic, validate its fields, and enqueue the event for the corresponding account. The [Bybit tutorial](/sdk/bybit/javascript/tutorial) supplies the authenticated subscription; it should not be replaced by a generic Node.js stream wrapper.

The `apply` function must finish the write or state transition before resolving. Starting another unawaited write inside it would recreate the concurrency problem behind the queue.

<span id="what-private-exchange-events-can-include"></span><span id="are-websocket-messages-the-same-as-node-js-streams"></span><span id="can-i-use-a-private-account-stream-instead-of-rest-polling"></span>

## Treat Failure as a State Problem {#private-websocket-events-rest-polling-and-nodejs-streams-compared}

`onFailure` must return synchronously without throwing. It should mark the affected consumer as needing repair and report the cause. Clearing queued events is not successful recovery. The active operation may also complete after failure, so its owner must prevent a late result from making an abandoned recovery attempt current again.

The [account initializer](/blog/account-state-management-for-crypto-apis-nodejs-guide) establishes missing values and resolves snapshot/event races. The queue preserves processing order; it cannot determine which REST response is newer than a private event or reconstruct messages that never arrived.

<span id="select-the-sdk-that-matches-the-exchange-integration"></span>

## Check the Processing Boundary {#use-siebly-sdks-as-the-nodejs-integration-layer}

Hold the first `apply` promise open and confirm the second event does not start. Release it and check arrival order. Then reject an operation and fill the queue past its limit. Each failure should be visible once, and further events should be refused until a new consumer is initialized.

During shutdown, stop accepting new work and decide whether pending operations can be drained. A process restart needs durable state and [scoped recovery](/blog/crypto-websocket-reliability-account-state), not a larger in-memory queue.

## Related articles

- [Deduplicating WebSocket Messages in Node.js](/blog/websocket-message-deduplication-in-nodejs-a-practical-guide)
- [Checking Bybit Unified Account Compatibility](/blog/bybit-unified-account-api-in-nodejs-a-v5-engineering-guide)
- [Recording Public Trades with Node.js and SQLite](/blog/public-market-data-collection-in-nodejs-a-practical-engineering-guide)


## Related Siebly Resources

- [account initializer](/blog/account-state-management-for-crypto-apis-nodejs-guide)
- [scoped recovery](/blog/crypto-websocket-reliability-account-state)
