Skip to main content

NATS WebSocket

Direct connection to NATS message broker via WebSocket, bypassing the backend entirely for ultra-low latency streaming.

Try it Online

Open NATS Client — See direct broker connection in action!

Overview

NATS WebSocket provides direct client-to-broker connectivity using the native NATS WebSocket interface. This eliminates the backend hop, achieving the lowest possible latency for real-time data.

Data Flow

The NATS protocol is the fastest option because it connects directly to the message broker:

NATS Data Flow

StepComponentActionLatency
1PostgresINSERT INTO rounds-
2DebeziumReads WAL, publishes to NATS~1-5ms
3NATSDelivers directly to browser<1ms
4Browsernats.ws subscription-

Total end-to-end latency: <1ms (excluding Debezium CDC)

Why is NATS the fastest?

NATS skips the Consumer, Redis, and Backend layers. The browser connects directly to the message broker.

Connection Details

PropertyValue
ProtocolWebSocket (NATS native)
Port8443
Latency0.5-2ms
DirectionBidirectional
Librarynats.ws (ESM)

Architecture

PostgreSQL → Debezium → NATS JetStream → Client (direct)

WebSocket :8443

Unlike other protocols that go through the backend, NATS WebSocket connects directly to the message broker:

  • No Backend Hop: Client subscribes directly to NATS subjects
  • Native Protocol: Uses nats.ws library for full NATS functionality
  • Subject Patterns: Subscribe to specific games or wildcards

Subject Patterns

NATS organizes messages using subjects (topics):

rounds.game.crash       # Crash game results only
rounds.game.double # Double game results only
rounds.game.wall-street # Wall Street game results only
rounds.game.slot # Slot game results only
rounds.game.* # All games (wildcard)
rounds.type.multiplier # All multiplier-type games
rounds.type.color # All color-type games

Client Implementation

JavaScript (Browser)

import { connect, StringCodec } from 'nats.ws';

async function connectToNATS() {
// Connect to NATS WebSocket
const nc = await connect({
servers: 'ws://localhost:8443'
});

console.log('Connected to NATS');

const sc = StringCodec();

// Subscribe to all game results
const sub = nc.subscribe('rounds.game.*');

for await (const msg of sub) {
const data = JSON.parse(sc.decode(msg.data));
const subject = msg.subject;
const game = subject.split('.').pop();

console.log(`[${game}]`, data);
handleResult(game, data);
}
}

function handleResult(game, result) {
const extras = typeof result.extras === 'string'
? JSON.parse(result.extras)
: result.extras;

console.log(`Game: ${game}, Round: ${result.round_id}`);
console.log(`Result:`, extras);
}

connectToNATS().catch(console.error);

Using ESM Import

<script type="module">
import { connect, StringCodec } from 'https://esm.sh/[email protected]';

const nc = await connect({ servers: 'ws://localhost:8443' });
const sc = StringCodec();

// Subscribe to crash game only
const sub = nc.subscribe('rounds.game.crash');

for await (const msg of sub) {
const result = JSON.parse(sc.decode(msg.data));
updateUI(result);
}
</script>

Message Format

Messages received from NATS have this structure:

{
"id": 12345,
"game_slug": "crash",
"game_type": "multiplier",
"round_id": 67890,
"started_at": "2024-01-15T10:30:00Z",
"finished_at": "2024-01-15T10:30:15Z",
"extras": {
"point": "2.47"
}
}

Game-Specific Extras

Crash (multiplier):

{ "point": "2.47" }

Double (color):

{ "color": "red", "number": 7 }

Wall Street (trending):

{ "trending": "up", "multiplier": "1.85" }

Slot (slots):

{ "reels": ["7", "7", "BAR"], "multiplier": "3.50" }

Latency Tracking

Track end-to-end latency by comparing timestamps:

for await (const msg of sub) {
const receiveTime = Date.now();
const result = JSON.parse(sc.decode(msg.data));

// If message includes timestamp
if (result.timestamp) {
const latency = receiveTime - result.timestamp;
console.log(`Latency: ${latency}ms`);
}
}

Connection Management

Reconnection Handling

const nc = await connect({
servers: 'ws://localhost:8443',
reconnect: true,
maxReconnectAttempts: -1, // Infinite retries
reconnectTimeWait: 1000, // 1 second between attempts
});

// Connection events
nc.closed().then(() => {
console.log('Connection closed');
});

// Status updates
(async () => {
for await (const status of nc.status()) {
console.log(`Status: ${status.type}`);
}
})();

Graceful Shutdown

// Drain subscriptions before closing
await nc.drain();

// Or force close
await nc.close();

When to Use NATS WebSocket

Best For

  • Ultra-low latency requirements (< 2ms)
  • High-frequency updates (100+ msgs/sec)
  • Direct broker access without backend
  • Flexible subscriptions with wildcard patterns
  • Multiple game monitoring simultaneously

Considerations

  • Requires NATS WebSocket port (8443) accessible
  • Client manages reconnection logic
  • No HTTP fallback if WebSocket fails
  • Browser must support ES modules

Comparison with Other Protocols

AspectNATS WSWebSocketSSEREST
Latency0.5-2ms1-5ms10-50ms50-200ms
DirectionBidirectionalBidirectionalServer→ClientRequest/Response
WildcardsYesNoNoNo
Direct BrokerYesNoNoNo
Auto-reconnectConfigurableManualNativeN/A

Live Demo

Try the NATS WebSocket demo at /nats.html:

http://localhost:3000/nats.html

Features:

  • Real-time connection to NATS
  • Wildcard subscription to all games
  • Latency measurement per message
  • Message count and statistics
  • Connection status indicator

Troubleshooting

Connection Refused

Error: connection refused

Ensure NATS is running with WebSocket enabled:

# Check NATS is running
docker compose ps nats

# Verify WebSocket port
curl -I http://localhost:8443

Invalid Subject

Error: invalid subject

Subjects must follow NATS naming conventions:

  • No spaces
  • Alphanumeric with dots (.) and wildcards (*, >)

CORS Issues

NATS WebSocket typically doesn't have CORS restrictions, but if running behind a proxy:

location /nats {
proxy_pass http://nats:8443;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}