Skip to main content

WebSocket Connection

Relay uses persistent WebSocket connections for real-time communication. Your app connects once with its Bearer token and keeps the connection open to send events and receive replies.

Connection Details

Endpoint: wss://api.relay.ckgworks.com/v1/ws/app

Authentication: Bearer token in the Authorization header

Authorization: Bearer rlk_yourapp_x8k2m9p...

Connection Lifecycle

1. Initial Connection

When you connect, send the Bearer token in the WebSocket handshake header:

import asyncio
import websockets
import json

async def connect_to_relay():
token = "rlk_yourapp_x8k2m9p..."
uri = "wss://api.relay.ckgworks.com/v1/ws/app"

headers = {
"Authorization": f"Bearer {token}"
}

async with websockets.connect(uri, subprotocols=["chat"]) as websocket:
# Connection established
await handle_connection(websocket)

Relay validates your token on the handshake. If the token is invalid or revoked, the connection is rejected immediately.

2. Keeping the Connection Alive

Once connected, maintain the connection with periodic heartbeats:

async def send_heartbeat(websocket):
while True:
await asyncio.sleep(30) # Send every 30 seconds
ping_message = json.dumps({"type": "ping"})
await websocket.send(ping_message)

# Wait for pong response
try:
response = await asyncio.wait_for(websocket.recv(), timeout=5)
pong = json.loads(response)
if pong.get("type") != "pong":
print("Warning: expected pong, got", pong.get("type"))
except asyncio.TimeoutError:
print("No pong received, connection may be stale")

Relay responds with a {"type": "pong"} message. If you don't receive a response within a few seconds, consider the connection unhealthy.

3. Handling Disconnections

Always implement reconnection logic with exponential backoff:

async def connect_with_retry(token, max_retries=5):
retry_count = 0
base_delay = 1 # Start with 1 second

while retry_count < max_retries:
try:
uri = "wss://api.relay.ckgworks.com/v1/ws/app"
headers = {"Authorization": f"Bearer {token}"}

async with websockets.connect(uri, subprotocols=["chat"]) as websocket:
print(f"Connected successfully (attempt {retry_count + 1})")
retry_count = 0 # Reset on successful connection
await handle_connection(websocket)

except Exception as e:
print(f"Connection failed: {e}")
retry_count += 1

if retry_count >= max_retries:
print(f"Max retries ({max_retries}) exceeded")
break

# Exponential backoff: 1s, 2s, 4s, 8s, 16s (capped)
delay = min(base_delay * (2 ** (retry_count - 1)), 60)
print(f"Retrying in {delay} seconds...")
await asyncio.sleep(delay)

Exponential Backoff Strategy

Recommended exponential backoff parameters:

  • Initial delay: 1 second
  • Max delay: 60 seconds
  • Multiplier: 2x per retry
  • Jitter: Optional, add ±10% randomness to avoid thundering herd
import random

def calculate_backoff(retry_count, base_delay=1, max_delay=60):
delay = min(base_delay * (2 ** retry_count), max_delay)
# Add jitter: ±10%
jitter = delay * 0.1 * (random.random() - 0.5)
return delay + jitter

Node.js Example (ws library)

const WebSocket = require('ws');

const token = 'rlk_yourapp_x8k2m9p...';
const uri = 'wss://api.relay.ckgworks.com/v1/ws/app';

const ws = new WebSocket(uri, {
headers: {
Authorization: `Bearer ${token}`
}
});

ws.on('open', () => {
console.log('Connected to Relay');

// Start heartbeat
const heartbeatInterval = setInterval(() => {
ws.send(JSON.stringify({ type: 'ping' }));
}, 30 * 1000);

ws.on('close', () => clearInterval(heartbeatInterval));
});

ws.on('message', (data) => {
const message = JSON.parse(data);

if (message.type === 'pong') {
console.log('Pong received');
} else if (message.type === 'token') {
console.log('Token:', message.token);
} else if (message.type === 'reply') {
console.log('Full reply:', message.reply);
}
});

ws.on('error', (error) => {
console.error('WebSocket error:', error);
});

ws.on('close', () => {
console.log('Connection closed, reconnecting...');
setTimeout(() => connectWithRetry(), 1000);
});

// Reconnection logic
let retryCount = 0;
const maxRetries = 5;

function connectWithRetry() {
if (retryCount >= maxRetries) {
console.error('Max retries exceeded');
return;
}

try {
const newWs = new WebSocket(uri, {
headers: { Authorization: `Bearer ${token}` }
});

newWs.on('open', () => {
console.log('Reconnected');
retryCount = 0;
});

newWs.on('close', connectWithRetry);
} catch (error) {
const delay = Math.min(1000 * Math.pow(2, retryCount), 60000);
retryCount++;
setTimeout(connectWithRetry, delay);
}
}

Connection Best Practices

Do

  • Keep one persistent connection open per app instance
  • Send heartbeats every 30-60 seconds
  • Use exponential backoff for reconnection
  • Log connection state changes
  • Handle all message types (token, reply, error)

Don't

  • Create a new connection for each event
  • Ignore disconnection events
  • Use fixed delays for retries
  • Hardcode timeouts shorter than 5 seconds
  • Send custom heartbeat patterns

Troubleshooting

Connection rejected immediately?

  • Check your Bearer token is valid (prefix rlk_)
  • Verify the token hasn't been rotated or revoked
  • Confirm you're using the correct WebSocket endpoint

Connection drops frequently?

  • Add more aggressive heartbeats (every 15-30 seconds)
  • Check your network for packet loss
  • Ensure exponential backoff is implemented correctly

Pong never arrives?

  • Increase timeout from 5 to 10 seconds initially
  • Verify network connectivity to api.relay.ckgworks.com
  • Check firewall/proxy isn't filtering WebSocket frames