Real-Time Queue Statistics WebSocket Returning Negative Wait Time Values

Building a custom real-time wallboard that displays live queue statistics via the Notification API WebSocket. I subscribe to v2.routing.queues.{queueId}.observations and render the metrics on a React dashboard.

The problem: the oWaiting metric occasionally returns negative values. Specifically, I am seeing values like -3, -1, -7 for interactions waiting in queue. This happens roughly every 15-20 minutes, persists for 30-60 seconds, and then corrects itself back to a valid count.

During the negative count period, the oInteracting metric also spikes by approximately the same magnitude. So if oWaiting drops to -3, oInteracting jumps up by about 3 more than expected.

My WebSocket subscription:

const topic = `v2.routing.queues.${queueId}.observations`;
notificationHandler.addSubscription(topic, (data) => {
 const metrics = data.eventBody.data;
 const waiting = metrics.find(m => m.metric === 'oWaiting');
 console.log(`Waiting: ${waiting?.stats?.count}`);
});

The negative values cause our wallboard to display nonsensical data. The Genesys admin UI queue view never shows negative values for the same queue at the same time. What is different about the WebSocket observation data?

This is a well-known issue with the observations WebSocket topic, and the root cause is actually quite interesting from a distributed systems perspective.

The observation metrics are computed from an eventually consistent event stream. When an interaction transitions from “waiting” to “interacting” (agent answers), two separate events are emitted: a decrement event for oWaiting and an increment event for oInteracting. These events are processed asynchronously and can arrive at the notification channel in the wrong order.

When the decrement for oWaiting arrives before the previous increment was processed (due to a burst of simultaneous agent answers), the running counter temporarily goes negative.

The Genesys admin UI does not show this because it uses a different data path. The admin UI polls the GET /api/v2/routing/queues/{queueId}/observations REST endpoint, which returns a snapshot computed from the fully reconciled state rather than the raw event stream.

For your wallboard, implement a floor clamp:

const sanitizedWaiting = Math.max(0, waiting?.stats?.count ?? 0);

This is exactly what the Genesys admin UI does internally. The negative values self-correct within 30-60 seconds as the event stream reconciles, so clamping to zero gives you the correct UX without losing accuracy over time.

Oh man, we hit this exact thing when we built our first wallboard! The floor clamp is definitely the right approach for the display layer.

But if you want truly accurate real-time counts without the reconciliation artifacts, there is a hybrid approach that works great even for small teams. Instead of relying solely on the WebSocket observations, use the WebSocket for change notifications but fetch the actual counts from the REST API on each change:

notificationHandler.addSubscription(topic, async () => {
 // Use the WebSocket event as a trigger, not a data source
 const response = await routingApi.getRoutingQueueObservations(queueId);
 updateDashboard(response);
});

The REST endpoint returns the reconciled snapshot every time. The WebSocket tells you WHEN to fetch, so you avoid polling. This gives you sub-second responsiveness with accurate data.

The downside is more API calls, but for a single wallboard monitoring 5-10 queues, the call volume is negligible.

Both approaches work. Just want to point out that the hybrid approach has a rate limit consideration. The GET /api/v2/routing/queues/{queueId}/observations endpoint has a rate limit of 15 requests per second per org (not per queue). If your wallboard monitors 10 queues and each queue gets 2+ observation events per second during peak, you will hit the rate limit within minutes.

Throttle your REST fetches with a debounce timer per queue. Do not fetch more than once every 3 seconds per queue:

const fetchTimers = {};

notificationHandler.addSubscription(topic, () => {
 if (!fetchTimers[queueId]) {
 fetchTimers[queueId] = setTimeout(async () => {
 const response = await routingApi.getRoutingQueueObservations(queueId);
 updateDashboard(response);
 delete fetchTimers[queueId];
 }, 3000);
 }
});

This caps your API usage at roughly 3-4 calls per second for 10 queues, well within the rate limit.