Skip to content

WebSocket Connection Lifecycle

Manage the full lifecycle of a Rhombus WebSocket connection — perform STOMP handshakes, send heartbeats, handle reconnections, and close gracefully.

On this page

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 your credential. This example uses an API key; OAuth access tokens and federated tokens work too (see WebSocket authentication):

python
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 answers before the upgrade completes with an HTTP error and a JSON body whose errorCode says why: 401 when the credential is missing, unknown, expired or revoked, 403 when it's valid but not allowed (for example, a partner credential without x-auth-org). See WebSocket authentication.

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
python
subscribe_frame = build_stomp_frame("SUBSCRIBE", {
    "id": "sub-0",
    "destination": f"/topic/change/{org_uuid}"
})
ws.send(subscribe_frame)

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.

python
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

python
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

python
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:

python
# 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.

Delivery guarantees

The event WebSocket delivers each message at most once: messages aren't acknowledged, stored or replayed, so anything published while you are disconnected is lost, and there's no ordering guarantee. After every reconnect, read what you missed from the REST API (see Build an event pipeline), and order events by their timestamps. The event reference has the details.

When the server closes the connection

CloseCauseWhat to do
STOMP ERROR, then close code 1008The credential was deleted or revoked, or its role changed. The server re-checks the credential about once a minute, so this happens within 60 secondsCheck the credential before reconnecting; after a delete or revoke every handshake fails with 401
STOMP ERROR, then close code 1002A subscription to another organization's topic, or a frame the server doesn't acceptFix the subscription; reconnecting with the same frames fails the same way
Close with no ERROR frameMissed heartbeats, no CONNECT within 60 seconds, a slow client, or a network or server restartReconnect with backoff

Reconnection strategy

Network interruptions are inevitable. Implement automatic reconnection with backoff:

python
import time

def connect_with_retry(api_token, org_uuid, max_retries=10):
    """Connect to Rhombus WebSocket with exponential backoff."""
    for attempt in range(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)
            print("Connected successfully")
            return ws
        except Exception as e:
            delay = min(5 * (2 ** attempt), 60)
            print(f"Connection failed: {e}. Retrying in {delay}s...")
            time.sleep(delay)

    raise ConnectionError("Max retries exceeded")

Full reconnection loop

python
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
First frame60 secondsSend the STOMP CONNECT frame within this time of opening the socket, or it's closed
Slow client10 seconds or 512 KiBA client the server can't send to for 10 seconds, or that falls 512 KiB behind, is disconnected

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 the org UUID matches your credential's organization (or the x-auth-org client)
Reconnection loopThe credential was deleted, revoked or expired (401 on every handshake), or the organization is rate limited (429)Check the credential with a REST call; on 429 wait Retry-After and back off