Connection Lifecycle
This guide covers the full lifecycle of a Rhombus WebSocket connection: establishing the connection, performing the STOMP handshake, maintaining the connection with heartbeats, and handling disconnections gracefully.
Lifecycle Overview
┌─────────────────┐
│ 1. WebSocket │
│ Handshake │──── WSS + Auth Headers
└────────┬────────┘
▼
┌─────────────────┐
│ 2. STOMP │
│ CONNECT │──── Version + Heartbeat negotiation
└────────┬────────┘
▼
┌─────────────────┐
│ 3. SUBSCRIBE │──── Topic subscription
└────────┬────────┘
▼
┌─────────────────┐
│ 4. MESSAGE │◄─── Real-time events
│ Loop │───► Heartbeat ping/pong
└────────┬────────┘
▼
┌─────────────────┐
│ 5. DISCONNECT │──── Graceful shutdown
└─────────────────┘
Step 1: WebSocket Handshake
Initiate a secure WebSocket connection to the Rhombus endpoint with authentication headers:
import websocket
ws = websocket.create_connection(
"wss://ws.rhombussystems.com:8443/websocket?x-auth-scheme=api-token",
header={"x-auth-apikey": API_TOKEN},
timeout=10 # 10-second handshake timeout
)
A successful handshake upgrades the HTTP connection to WebSocket. If authentication fails, the server returns an HTTP error (401/403) before the upgrade completes.
Step 2: STOMP CONNECT
After the WebSocket connection is established, send a STOMP CONNECT frame to initialize the messaging protocol:
CONNECT Frame
CONNECT
accept-version:1.2
heart-beat:10000,10000
\x00
| Header | Value | Description |
|---|
accept-version | 1.2 | STOMP protocol version |
heart-beat | 10000,10000 | Client send/receive heartbeat interval in milliseconds |
Expected Response: CONNECTED Frame
CONNECTED
version:1.2
heart-beat:10000,10000
\x00
The server echoes back the negotiated version and heartbeat intervals.
Building STOMP Frames
STOMP frames follow a simple text-based format:
COMMAND\n
header1:value1\n
header2:value2\n
\n
body (optional)
\x00
- Each frame starts with a command name
- Headers are key-value pairs separated by colons, one per line
- An empty line separates headers from the body
- The frame ends with a null byte (
\x00)
def build_stomp_frame(command, headers=None, body=""):
"""Construct a STOMP protocol frame."""
frame = command + "\n"
if headers:
for key, value in headers.items():
frame += f"{key}:{value}\n"
frame += "\n"
frame += body
frame += "\x00"
return frame
# Send CONNECT
connect_frame = build_stomp_frame("CONNECT", {
"accept-version": "1.2",
"heart-beat": "10000,10000"
})
ws.send(connect_frame)
# Read CONNECTED response
response = ws.recv()
Step 3: Subscribe to a Topic
After the STOMP connection is established, subscribe to the organization change topic:
SUBSCRIBE Frame
SUBSCRIBE
id:sub-0
destination:/topic/change/{orgUuid}
\x00
| Header | Value | Description |
|---|
id | sub-0 | A client-defined subscription identifier |
destination | /topic/change/{orgUuid} | The topic to subscribe to |
subscribe_frame = build_stomp_frame("SUBSCRIBE", {
"id": "sub-0",
"destination": f"/topic/change/{org_uuid}"
})
ws.send(subscribe_frame)
Replace {orgUuid} with your organization’s UUID. Retrieve it via the REST API endpoint POST /api/org/getOrgV2.
Step 4: Message Loop and Heartbeats
Once subscribed, your application enters a message loop where it:
- Receives MESSAGE frames containing event data
- Sends heartbeats to keep the connection alive
- Receives heartbeats from the server
Heartbeat Mechanism
Heartbeats are single newline characters (\n) sent at the negotiated interval (10 seconds). Both the client and server send heartbeats. If either side stops receiving heartbeats, the connection is considered dead.
import threading
import time
def send_heartbeats(ws, stop_event):
"""Send STOMP heartbeats every 10 seconds."""
while not stop_event.is_set():
try:
ws.send("\n")
except Exception:
break
stop_event.wait(10)
# Start heartbeat sender in background
stop_heartbeat = threading.Event()
heartbeat_thread = threading.Thread(
target=send_heartbeats,
args=(ws, stop_heartbeat),
daemon=True
)
heartbeat_thread.start()
Parsing Incoming Frames
def parse_stomp_frame(raw):
"""Parse a raw STOMP frame into command, headers, and body."""
# Handle heartbeats (empty frames)
raw = raw.lstrip("\n\r")
if not raw or raw == "\x00":
return None # Heartbeat
raw = raw.rstrip("\x00")
parts = raw.split("\n\n", 1)
lines = parts[0].split("\n")
command = lines[0]
headers = {}
for line in lines[1:]:
if ":" in line:
key, value = line.split(":", 1)
headers[key] = value
body = parts[1] if len(parts) > 1 else ""
return {"command": command, "headers": headers, "body": body}
Message Loop
try:
while True:
raw = ws.recv()
frame = parse_stomp_frame(raw)
if frame is None:
continue # Heartbeat, skip
if frame["command"] == "MESSAGE":
payload = json.loads(frame["body"])
handle_event(payload)
except KeyboardInterrupt:
print("Shutting down...")
finally:
stop_heartbeat.set()
heartbeat_thread.join()
Step 5: Graceful Disconnection
Before closing the WebSocket, send a STOMP DISCONNECT frame:
# Send DISCONNECT frame
disconnect_frame = build_stomp_frame("DISCONNECT")
ws.send(disconnect_frame)
# Close WebSocket
ws.close()
This signals to the server that the client is intentionally disconnecting, allowing it to clean up resources immediately rather than waiting for a heartbeat timeout.
Reconnection Strategy
Network interruptions are inevitable. Implement automatic reconnection with backoff:
import time
def connect_with_retry(api_token, org_uuid, max_retries=None):
"""Connect to Rhombus WebSocket with automatic retry."""
attempt = 0
while max_retries is None or attempt < max_retries:
try:
ws = websocket.create_connection(
"wss://ws.rhombussystems.com:8443/websocket"
"?x-auth-scheme=api-token",
header={"x-auth-apikey": api_token},
timeout=10
)
stomp_connect(ws)
stomp_subscribe(ws, org_uuid)
return ws
except Exception as e:
attempt += 1
print(f"Connection failed: {e}. Retrying in 5 seconds...")
time.sleep(5)
raise ConnectionError("Max retries exceeded")
Full Reconnection Loop
while True:
ws = connect_with_retry(API_TOKEN, ORG_UUID)
try:
run_message_loop(ws)
except websocket.WebSocketConnectionClosedException:
print("Connection lost. Reconnecting...")
except KeyboardInterrupt:
print("User interrupted. Exiting.")
break
finally:
try:
ws.close()
except Exception:
pass
Connection Timeouts
| Timeout | Value | Description |
|---|
| Handshake | 10 seconds | Maximum time to complete the WebSocket upgrade |
| Heartbeat | 10 seconds | Interval between keep-alive pings |
| Dead connection | ~30 seconds | Approximate time before a missing heartbeat triggers disconnection |
Common Issues
| Symptom | Likely Cause | Solution |
|---|
| Connection drops every ~30s | Heartbeats not being sent | Implement the heartbeat sender thread |
CONNECTED frame never received | STOMP CONNECT frame malformed | Verify frame format includes null terminator \x00 |
| Messages not received | Wrong topic or missing org UUID | Verify org UUID matches your API token’s organization |
| Reconnection loop | Token expired or revoked | Check token validity via REST API |