Skip to main content

STOMP Protocol Reference

Rhombus uses STOMP 1.2 (Streaming Text Oriented Messaging Protocol) over WebSocket for real-time event delivery. This page is a protocol reference for developers who need to understand or implement the STOMP framing at a low level.

What is STOMP?

STOMP is a simple, text-based messaging protocol that provides an interoperable wire format for message brokers. It defines a small set of commands (frames) for connecting, subscribing, sending, and disconnecting. Rhombus uses STOMP because it provides structured pub/sub messaging over a standard WebSocket transport.

Frame Format

Every STOMP frame follows this structure:
COMMAND\n
header-key:header-value\n
header-key:header-value\n
\n
body (optional)
\x00
  • COMMAND: A single word identifying the frame type (e.g., CONNECT, MESSAGE)
  • Headers: Zero or more key:value pairs, one per line
  • Empty line: Separates headers from body
  • Body: Optional payload (typically JSON for MESSAGE frames)
  • Null byte (\x00): Terminates every frame

Example: Raw CONNECT Frame

CONNECT\n
accept-version:1.2\n
heart-beat:10000,10000\n
\n
\x00

Client-to-Server Frames

CONNECT

Initiates the STOMP session after the WebSocket connection is open.
HeaderRequiredValueDescription
accept-versionYes1.2STOMP protocol version
heart-beatYes10000,10000Client send interval and desired receive interval (ms)
CONNECT
accept-version:1.2
heart-beat:10000,10000

\x00

SUBSCRIBE

Subscribes to a destination topic to receive messages.
HeaderRequiredValueDescription
idYessub-0Client-defined subscription identifier
destinationYes/topic/change/{orgUuid}Topic path to subscribe to
SUBSCRIBE
id:sub-0
destination:/topic/change/a1b2c3d4-e5f6-7890-abcd-ef1234567890

\x00
The subscription id is client-defined. Use any unique string. If you create multiple subscriptions, each must have a distinct id.

DISCONNECT

Signals an intentional, clean disconnection.
DISCONNECT

\x00
No headers or body are required. After sending DISCONNECT, close the underlying WebSocket connection.

Server-to-Client Frames

CONNECTED

Sent by the server in response to a successful CONNECT.
HeaderValueDescription
version1.2Negotiated STOMP version
heart-beat10000,10000Server send/receive heartbeat intervals
CONNECTED
version:1.2
heart-beat:10000,10000

\x00

MESSAGE

Delivers an event from a subscribed topic.
HeaderValueDescription
destination/topic/change/{orgUuid}The topic this message belongs to
message-idServer-generatedUnique message identifier
subscriptionsub-0Matches the subscription id
MESSAGE
destination:/topic/change/a1b2c3d4-e5f6-7890-abcd-ef1234567890
message-id:msg-001
subscription:sub-0

{"entity":"POLICY_ALERT","entityUuid":"x1y2z3","type":"CREATE","timestampMs":1711843200000}
\x00
The body is always a JSON object containing the event payload.

Heartbeats

Heartbeats keep the connection alive and detect failures. They are negotiated during the CONNECT/CONNECTED exchange.

Format

A heartbeat is a single newline character:
\n
No command, no headers, no null terminator. Just \n.

Negotiation

The heart-beat header uses the format cx,cy where:
  • cx = minimum interval (ms) at which the client can send heartbeats
  • cy = desired interval (ms) at which the client wants to receive heartbeats
The Rhombus server uses 10000,10000 (10 seconds both directions).

Behavior

DirectionIntervalWhat Happens on Failure
Client to Server10 secondsServer may close the connection
Server to Client10 secondsClient should assume the connection is dead and reconnect

Implementation

# Sending heartbeats
import threading

def send_heartbeats(ws, stop_event, interval=10):
    while not stop_event.is_set():
        ws.send("\n")
        stop_event.wait(interval)

# Receiving heartbeats
def parse_frame(raw):
    raw = raw.strip("\n\r")
    if not raw or raw == "\x00":
        return None  # This is a heartbeat, safe to ignore
    # ... parse as normal STOMP frame

Frame Parsing Guide

Step-by-Step Parsing Algorithm

1. Read WebSocket text message
2. Strip leading newlines/carriage returns
3. If empty or just \x00 → heartbeat, return null
4. Strip trailing \x00
5. Split on "\n\n" (first occurrence) → [header_block, body]
6. Split header_block on "\n" → [command, header1, header2, ...]
7. For each header line, split on first ":" → key, value
8. Return {command, headers, body}

Edge Cases

InputResult
\nHeartbeat (null)
\n\n\nHeartbeat (null)
\x00Heartbeat (null)
Frame with no bodyBody is empty string
Header value containing :Split only on first colon

Complete Frame Exchange

Here is the full frame sequence for a typical session:
Client                                Server
  │                                      │
  │  CONNECT                             │
  │  accept-version:1.2                  │
  │  heart-beat:10000,10000              │
  │  \x00                               │
  ├─────────────────────────────────────►│
  │                                      │
  │                          CONNECTED   │
  │                          version:1.2 │
  │                   heart-beat:10000,0 │
  │                                \x00  │
  │◄─────────────────────────────────────┤
  │                                      │
  │  SUBSCRIBE                           │
  │  id:sub-0                            │
  │  destination:/topic/change/{org}     │
  │  \x00                               │
  ├─────────────────────────────────────►│
  │                                      │
  │  \n  (heartbeat)                     │
  ├─────────────────────────────────────►│
  │                                      │
  │                     \n  (heartbeat)  │
  │◄─────────────────────────────────────┤
  │                                      │
  │                            MESSAGE   │
  │            destination:/topic/...    │
  │                                      │
  │                  {"entity":"..."}    │
  │                                \x00  │
  │◄─────────────────────────────────────┤
  │                                      │
  │  \n  (heartbeat)                     │
  ├─────────────────────────────────────►│
  │                                      │
  │  DISCONNECT                          │
  │  \x00                               │
  ├─────────────────────────────────────►│
  │                                      │
  │           [WebSocket close]          │
  │◄─────────────────────────────────────┤

Differences from Full STOMP 1.2

The Rhombus implementation uses a subset of STOMP 1.2. Notable differences:
FeatureSTOMP 1.2 SpecRhombus Implementation
SEND commandSupportedNot used (server-push only)
UNSUBSCRIBESupportedNot needed (single topic)
ACK/NACKSupportedNot used (auto-acknowledge)
RECEIPTSupportedNot used
ERROR frameSupportedMay be sent on server errors
content-length headerOptionalNot required
Transaction supportSupportedNot used
This means you do not need a full STOMP client library. The protocol subset is simple enough to implement manually, as shown in the Code Examples.