STOMP over WebSocket Guide
STOMP (Simple Text Oriented Messaging Protocol) provides a structured messaging layer on top of WebSocket connections. While raw WebSocket connections give you a bidirectional communication channel, STOMP adds publish-subscribe semantics, message routing, and acknowledgment mechanisms. This makes STOMP over WebSocket ideal for applications requiring message queues, topic subscriptions, or integration with message brokers like RabbitMQ and ActiveMQ.
The STOMP protocol works as a WebSocket subprotocol, meaning it operates within the WebSocket connection after the initial WebSocket handshake. Instead of sending arbitrary text or binary data, clients and servers exchange structured STOMP frames with commands, headers, and bodies. This structure eliminates the need to build your own message format and routing logic.
Why Use STOMP over Raw WebSocket
Raw WebSocket connections provide a low-level communication primitive. You send messages and receive messages, but there is no built-in concept of topics, queues, or message routing. STOMP adds these features without requiring you to implement them from scratch.
With STOMP, you can subscribe to specific destinations like /topic/notifications or /queue/user.123. The server-side message broker handles routing messages to the appropriate subscribers. This is particularly useful when you need to broadcast messages to multiple clients (topics) or send messages to specific users (queues).
STOMP also integrates seamlessly with enterprise message brokers. RabbitMQ, ActiveMQ, and Apollo all support STOMP as a native protocol. This means your WebSocket clients can interact with the same messaging infrastructure used by your backend services.
STOMP Protocol Basics
STOMP uses a frame-based protocol similar to HTTP. Each frame consists of a command, headers, and an optional body. Frames are text-based and human-readable, which simplifies debugging.
A typical STOMP frame looks like this:
COMMAND
header1:value1
header2:value2
Body content here^@
The frame starts with a command name, followed by headers in key:value format, a blank line, the message body, and a null byte (^@) to mark the end.
Core STOMP Commands
The STOMP protocol defines several commands for client-server communication:
CONNECT: Initiates a STOMP session over the WebSocket connection. The client sends this as the first frame after the WebSocket handshake completes.
CONNECT
accept-version:1.2
host:example.com
^@
CONNECTED: Server response confirming the STOMP session is established.
SUBSCRIBE: Registers interest in a destination. The client receives messages sent to this destination.
SUBSCRIBE
id:sub-0
destination:/topic/notifications
^@
SEND: Publishes a message to a destination. Other subscribers to that destination will receive it.
SEND
destination:/app/message
content-type:application/json
{"text":"Hello"}^@
MESSAGE: Delivered from server to client when a subscribed destination receives a message.
DISCONNECT: Gracefully closes the STOMP session.
DISCONNECT
receipt:77
^@
These commands form the foundation of STOMP communication. The stompjs library abstracts these low-level details, but understanding the protocol helps when debugging or implementing custom logic.
Setting Up stompjs in the Browser
The stompjs library provides a JavaScript STOMP client for browsers and Node.js. The modern version is published as @stomp/stompjs on npm. This is the recommended library for new projects.
Install the library:
npm install @stomp/stompjs
For browser applications, you also need a WebSocket implementation. Modern browsers provide the native WebSocket API, so no additional dependency is required. For Node.js, you need the ws package.
Here is a basic stompjs client setup:
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'ws://localhost:8080/ws',
connectHeaders: {
login: 'user',
passcode: 'password'
},
debug: function (str) {
console.log('STOMP: ' + str);
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000
});
client.onConnect = function (frame) {
console.log('Connected: ' + frame);
// Subscribe to a topic
client.subscribe('/topic/notifications', function (message) {
console.log('Received: ' + message.body);
const payload = JSON.parse(message.body);
console.log(payload);
});
// Send a message
client.publish({
destination: '/app/hello',
body: JSON.stringify({ name: 'World' })
});
};
client.onStompError = function (frame) {
console.error('STOMP error: ' + frame.headers['message']);
console.error('Details: ' + frame.body);
};
client.activate();
The Client class from @stomp/stompjs manages the WebSocket connection and STOMP session. The brokerURL specifies the WebSocket endpoint. The connectHeaders object contains authentication credentials if required.
The onConnect callback executes when the STOMP session is established. This is where you subscribe to destinations and send initial messages. The subscribe method registers a callback that fires when messages arrive at the specified destination.
The publish method sends messages to a destination. The destination path determines how the server routes the message. Destinations starting with /app typically trigger server-side message handlers, while destinations starting with /topic or /queue route directly to subscribers.
Using StompClient and WebSocketStompClient
The terms StompClient and WebSocketStompClient often appear in STOMP documentation. These refer to different implementations and versions of STOMP clients.
In the JavaScript ecosystem, @stomp/stompjs provides the Client class. Older versions of the library used a different API with Stomp.client() and Stomp.over() functions. The modern Client class replaces these legacy APIs.
In the Java Spring ecosystem, WebSocketStompClient is a Spring Framework class that provides a STOMP client implementation. It is commonly used for testing STOMP endpoints or creating backend-to-backend STOMP connections.
Here is an example using the legacy stompjs API (for reference, not recommended for new projects):
// Legacy API (pre @stomp/stompjs)
const socket = new WebSocket('ws://localhost:8080/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/notifications', function(message) {
console.log(message.body);
});
});
The modern @stomp/stompjs API is cleaner and provides better error handling, automatic reconnection, and TypeScript support. Always use @stomp/stompjs for new projects.
Spring Boot STOMP Server Configuration
Spring Boot provides excellent support for STOMP over WebSocket through the spring-websocket and spring-messaging modules. The WebSocketMessageBrokerConfigurer interface allows you to configure STOMP endpoints and message routing.
Here is a complete Spring Boot STOMP configuration:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// Enable a simple in-memory message broker
config.enableSimpleBroker("/topic", "/queue");
// Prefix for messages routed to @MessageMapping methods
config.setApplicationDestinationPrefixes("/app");
// Prefix for user-specific destinations
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// Register the /ws endpoint for WebSocket connections
registry.addEndpoint("/ws")
.setAllowedOrigins("*");
// Register with SockJS fallback
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
}
The @EnableWebSocketMessageBroker annotation enables STOMP message handling. The configureMessageBroker method sets up message routing. The enableSimpleBroker method activates an in-memory broker that handles destinations matching the specified prefixes (/topic and /queue).
The setApplicationDestinationPrefixes method defines the prefix for messages that should be routed to @MessageMapping annotated methods in your controllers. When a client sends a message to /app/hello, Spring routes it to a method annotated with @MessageMapping("/hello").
The registerStompEndpoints method defines the WebSocket endpoint URL. Clients connect to this endpoint to establish the WebSocket connection. The example also shows SockJS fallback support for browsers that do not support WebSocket natively.
Here is a controller with a message handler:
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class MessageController {
@MessageMapping("/hello")
@SendTo("/topic/notifications")
public NotificationMessage greeting(HelloMessage message) throws Exception {
Thread.sleep(100); // Simulate processing delay
return new NotificationMessage("Hello, " + message.getName() + "!");
}
}
When a client sends a message to /app/hello, this method processes it and returns a response. The @SendTo annotation automatically publishes the return value to /topic/notifications, which all subscribers receive.
For more details on Spring WebSocket setup, see the Java Spring WebSocket guide.
Using RabbitMQ as a STOMP Broker
The simple in-memory broker works well for development and small applications, but production systems benefit from a dedicated message broker like RabbitMQ. RabbitMQ provides persistence, clustering, and advanced routing capabilities.
RabbitMQ supports STOMP through a plugin. Enable the STOMP plugin:
rabbitmq-plugins enable rabbitmq_stomp
rabbitmq-plugins enable rabbitmq_web_stomp
The rabbitmq_stomp plugin provides STOMP over TCP, while rabbitmq_web_stomp provides STOMP over WebSocket. By default, RabbitMQ listens for STOMP WebSocket connections on port 15674.
Update your Spring Boot configuration to use RabbitMQ:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// Use RabbitMQ as the message broker
config.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost("localhost")
.setRelayPort(61613)
.setClientLogin("guest")
.setClientPasscode("guest");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*");
}
}
The enableStompBrokerRelay method configures Spring to forward messages to RabbitMQ instead of using the in-memory broker. The setRelayHost and setRelayPort specify the RabbitMQ STOMP endpoint (default port 61613 for STOMP over TCP).
Add the required dependencies to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-reactor-netty</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
With RabbitMQ, messages persist across application restarts, and you can scale your application horizontally. Multiple Spring Boot instances can connect to the same RabbitMQ broker and share message routing.
RxStomp for Reactive Applications
RxStomp wraps the stompjs client with RxJS observables, providing a reactive programming model. This is useful for Angular applications or any JavaScript application using RxJS.
Install RxStomp:
npm install @stomp/rx-stomp
RxStomp provides an observable-based API:
import { RxStomp } from '@stomp/rx-stomp';
const rxStomp = new RxStomp();
rxStomp.configure({
brokerURL: 'ws://localhost:8080/ws',
connectHeaders: {
login: 'user',
passcode: 'password'
},
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
reconnectDelay: 5000,
debug: (msg) => console.log(new Date(), msg)
});
rxStomp.activate();
// Subscribe using observables
rxStomp.watch('/topic/notifications').subscribe(message => {
console.log('Received:', message.body);
const payload = JSON.parse(message.body);
console.log(payload);
});
// Publish messages
rxStomp.publish({
destination: '/app/hello',
body: JSON.stringify({ name: 'World' })
});
The watch method returns an RxJS observable that emits messages from the specified destination. You can use RxJS operators to transform, filter, or combine message streams.
RxStomp also provides observables for connection state:
import { RxStompState } from '@stomp/rx-stomp';
rxStomp.connectionState$.subscribe(state => {
console.log('Connection state:', RxStompState[state]);
});
This reactive approach integrates well with Angular’s change detection and makes it easier to manage complex message flows.
Authentication and Authorization
STOMP connections typically require authentication. The STOMP CONNECT frame includes login and passcode headers for credentials.
In Spring Boot, you can integrate STOMP authentication with Spring Security:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/app/**").authenticated()
.simpSubscribeDestMatchers("/topic/**", "/queue/**").authenticated()
.anyMessage().denyAll();
}
@Override
protected boolean sameOriginDisabled() {
return true; // Disable CSRF for WebSocket
}
}
This configuration requires authentication for all /app messages and topic subscriptions. You can also implement user-specific queues:
@Controller
public class MessageController {
@MessageMapping("/private")
@SendToUser("/queue/reply")
public String handlePrivateMessage(String message, Principal principal) {
return "Hello " + principal.getName() + ", you said: " + message;
}
}
The @SendToUser annotation sends the response to a user-specific queue. Spring automatically creates a unique queue for each authenticated user. The client subscribes to /user/queue/reply, and Spring routes it to the correct user-specific destination.
Client-side authentication:
const client = new Client({
brokerURL: 'ws://localhost:8080/ws',
connectHeaders: {
login: 'username',
passcode: 'password'
},
onConnect: (frame) => {
console.log('Authenticated');
client.subscribe('/user/queue/reply', (message) => {
console.log('Private message:', message.body);
});
}
});
client.activate();
For token-based authentication (JWT), you can pass the token as a query parameter or custom header during the WebSocket handshake. Spring Security intercepts the handshake and validates the token before allowing the STOMP connection.
Testing STOMP WebSocket Connections
Testing STOMP connections requires verifying both the WebSocket layer and the STOMP protocol layer. You can test stomp websocket connections using browser developer tools, standalone clients, or automated tests.
For manual testing, use the browser console:
const client = new Client({
brokerURL: 'ws://localhost:8080/ws',
debug: (str) => console.log('STOMP:', str),
onConnect: () => {
console.log('Connected');
client.subscribe('/topic/test', (msg) => {
console.log('Message:', msg.body);
});
client.publish({
destination: '/app/test',
body: 'Hello from console'
});
}
});
client.activate();
The debug function logs all STOMP frames, which helps verify the protocol communication.
For automated testing in Spring Boot, use WebSocketStompClient:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebSocketTest {
@LocalServerPort
private int port;
@Test
public void testStompConnection() throws ExecutionException,
InterruptedException, TimeoutException {
WebSocketStompClient stompClient = new WebSocketStompClient(
new StandardWebSocketClient());
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
String url = "ws://localhost:" + port + "/ws";
StompSession session = stompClient.connect(url,
new StompSessionHandlerAdapter() {})
.get(5, TimeUnit.SECONDS);
session.subscribe("/topic/test", new StompSessionHandlerAdapter() {
@Override
public void handleFrame(StompHeaders headers, Object payload) {
System.out.println("Received: " + payload);
}
});
session.send("/app/test", "Hello");
Thread.sleep(1000);
session.disconnect();
}
}
This test connects to the STOMP endpoint, subscribes to a topic, sends a message, and verifies the response.
For more comprehensive testing strategies, see how to test websockets.
WebSocket Subprotocol Negotiation
STOMP operates as a websocket subprotocol, negotiated during the WebSocket handshake. The client includes Sec-WebSocket-Protocol: stomp in the handshake request, and the server responds with the same header to confirm support.
The stompjs library handles subprotocol negotiation automatically. However, when implementing a custom client or debugging connections, understanding the subprotocol websocket negotiation is important.
A typical handshake request:
GET /ws HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: v12.stomp, v11.stomp, v10.stomp
The server responds:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: v12.stomp
The Sec-WebSocket-Protocol header in the response confirms the STOMP version. After the handshake, all communication uses STOMP frames instead of raw WebSocket messages.
Spring Boot automatically handles websocket sub protocols when you configure STOMP endpoints. The framework negotiates the STOMP version and manages the protocol state.
Performance Considerations
STOMP adds overhead compared to raw WebSocket connections. Each message includes command headers and frame delimiters. For high-throughput applications, this overhead can impact performance.
Consider these optimization strategies:
Use binary messages: While STOMP is text-based, you can send binary payloads in the message body. Encode data as MessagePack or Protocol Buffers instead of JSON to reduce message size.
Batch messages: Instead of sending individual messages, batch multiple updates into a single STOMP message. The application code unpacks the batch and processes each item.
Compression: Enable WebSocket compression (permessage-deflate extension) to reduce bandwidth usage. Most browsers and servers support this transparently.
Connection pooling: For backend services connecting to STOMP brokers, maintain a pool of connections rather than creating new connections for each operation.
Heartbeats: Configure appropriate heartbeat intervals. Too frequent heartbeats waste bandwidth, while infrequent heartbeats delay detection of broken connections.
const client = new Client({
brokerURL: 'ws://localhost:8080/ws',
heartbeatIncoming: 10000, // Expect heartbeat every 10 seconds
heartbeatOutgoing: 10000 // Send heartbeat every 10 seconds
});
Common STOMP Patterns
Several patterns emerge when building applications with stomp websocket:
Request-Response: A client sends a message and expects a response. Use a temporary queue for replies.
const replyQueue = '/temp-queue/reply-' + Math.random();
client.subscribe(replyQueue, (message) => {
console.log('Response:', message.body);
});
client.publish({
destination: '/app/request',
headers: { 'reply-to': replyQueue },
body: 'Request payload'
});
Broadcast: Send a message to all connected clients. Use topic destinations.
// Server-side broadcast
messagingTemplate.convertAndSend("/topic/broadcast", payload);
// Client-side subscription
client.subscribe('/topic/broadcast', (message) => {
console.log('Broadcast:', message.body);
});
User-specific messages: Send messages to a specific user. Use user queues.
// Server sends to specific user
messagingTemplate.convertAndSendToUser(username, "/queue/private", payload);
// Client subscribes to user queue
client.subscribe('/user/queue/private', (message) => {
console.log('Private message:', message.body);
});
Frequently Asked Questions
What is the difference between stompjs and @stomp/stompjs?
The @stomp/stompjs package is the modern, actively maintained version of the STOMP JavaScript client. It provides better error handling, TypeScript support, and a cleaner API compared to the older stompjs package. Always use @stomp/stompjs for new projects. The package includes the Client class for standard usage and integrates with @stomp/rx-stomp for reactive applications.
Can I use STOMP without WebSocket?
Yes, STOMP is a protocol that can run over multiple transports. The original STOMP protocol runs over TCP sockets. RabbitMQ and ActiveMQ support STOMP over TCP on port 61613. However, for browser-based applications, STOMP over WebSocket is the standard approach because browsers cannot create raw TCP connections. The javascript stomp libraries like stompjs focus on the WebSocket transport.
How do I handle reconnection in STOMP clients?
The @stomp/stompjs library includes automatic reconnection. Set the reconnectDelay option to enable it. The client will attempt to reconnect after connection loss, using exponential backoff. You can also listen to connection state changes and implement custom reconnection logic. When the client reconnects, you must re-subscribe to all destinations because subscriptions are not persisted across connections.
What is the difference between /topic and /queue in spring boot stomp?
In STOMP messaging, /topic destinations implement publish-subscribe semantics where messages are delivered to all active subscribers. The /queue destinations implement point-to-point semantics where each message is delivered to only one subscriber. However, the actual behavior depends on the message broker configuration. In Spring’s simple broker, both /topic and /queue work identically as broadcast destinations. When using RabbitMQ or ActiveMQ as the broker, these prefixes map to actual queues and exchanges with the expected semantics.