Skip to main content

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
HeaderValueDescription
accept-version1.2STOMP protocol version
heart-beat10000,10000Client 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
HeaderValueDescription
idsub-0A 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:
  1. Receives MESSAGE frames containing event data
  2. Sends heartbeats to keep the connection alive
  3. 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

TimeoutValueDescription
Handshake10 secondsMaximum time to complete the WebSocket upgrade
Heartbeat10 secondsInterval between keep-alive pings
Dead connection~30 secondsApproximate time before a missing heartbeat triggers disconnection

Common Issues

SymptomLikely CauseSolution
Connection drops every ~30sHeartbeats not being sentImplement the heartbeat sender thread
CONNECTED frame never receivedSTOMP CONNECT frame malformedVerify frame format includes null terminator \x00
Messages not receivedWrong topic or missing org UUIDVerify org UUID matches your API token’s organization
Reconnection loopToken expired or revokedCheck token validity via REST API