Node.js WebSocket reconnect loop for Genesys Notification API

I’m trying to build a simple Node.js script to listen to queue events via the Notification API WebSocket. The connection works fine initially, but when the server drops it, my reconnection logic gets stuck in a tight loop and hits rate limits. I’m not using the official SDK right now, just ws. Here’s the gist of what I have:

const WebSocket = require('ws');

function connect() {
 const ws = new WebSocket('wss://api-us-east-1.genesyscloud.com/v2/analytics/events/ws');
 
 ws.on('open', () => {
 ws.send(JSON.stringify({
 "api-version": "1",
 "events": ["queue:summary"]
 }));
 });

 ws.on('close', (code, reason) => {
 console.log(`Connection closed: ${code}`);
 // This feels wrong but I can't figure out the right delay
 setTimeout(connect, 1000);
 });
}

connect();

The issue is that code is often 1000 or 1001, and I’m not sure if I should be backing off exponentially or just waiting a fixed amount. Also, do I need to re-send the subscription payload every time I reconnect, or does Genesys remember it? The docs are a bit vague on the reconnection best practices for raw WebSocket usage. I just need the connection to stay alive without flooding the logs.

Are you handling the close event code or just assuming all drops are server-side?

Cause:
The tight loop happens because you are reconnecting immediately on every disconnect. If the server sends a 1001 or 4001 close code, it means the connection was invalid or overloaded. Reconnecting instantly triggers rate limiting. Also, without exponential backoff, you hammer the auth endpoint.

Solution:
Check the closeCode before reconnecting. Implement a simple backoff mechanism. Here is a cleaner pattern using the official SDK which handles reconnections automatically, but if you stick to ws, look at this logic:

ws.on('close', (code, reason) => {
 if (code === 1000 || code === 1001) {
 console.log('Normal closure or going away');
 return;
 }
 
 let delay = 1000 * Math.pow(2, retryCount);
 setTimeout(() => connect(), delay);
});

Don’t forget to reset retryCount on successful open. The SDK platformClient.NotificationsApi is better for this anyway since it manages the queue subscription lifecycle.

You’re absolutely right about the backoff, but there’s a bigger gotcha here that trips people up constantly. The Notification API WebSocket doesn’t just drop connections randomly. It validates your auth token on every reconnect. If your access token expires while the socket is down, your next reconnect attempt will fail with a 401, and if you’re not handling that error state properly, you’ll spin forever trying to authenticate with a dead token.

Don’t just retry the WebSocket connection. You need to check the closeCode. If it’s 1000 (normal closure) or 1001 (going away), you might just reconnect. But if it’s anything else, especially 4001 (bad request/auth), you need to refresh your OAuth token first.

Here’s how I handle the reconnect logic in my hybrid setups. I use a simple exponential backoff and check the token validity before reconnecting:

let reconnectAttempts = 0;
const maxReconnectAttempts = 5;

ws.on('close', (code, reason) => {
 console.log(`WebSocket closed with code: ${code}`);
 
 // If it's a normal closure or auth error, stop reconnecting
 if (code === 1000 || code === 4001) {
 console.log('Connection closed normally or auth failed. Stopping reconnect.');
 return;
 }

 if (reconnectAttempts < maxReconnectAttempts) {
 const backoffTime = Math.pow(2, reconnectAttempts) * 1000;
 console.log(`Reconnecting in ${backoffTime}ms...`);
 
 setTimeout(() => {
 reconnectAttempts++;
 connect(); // Your connect function that handles token refresh if needed
 }, backoffTime);
 } else {
 console.log('Max reconnect attempts reached. Giving up.');
 }
});

Also, make sure you’re using the correct scope. You need notification:web:read for WebSocket notifications. If you’re using a client credentials grant, ensure the service account has the right permissions. I’ve seen too many scripts fail because the token was valid but lacked the scope. It’s a subtle issue that wastes hours.