I still remember the first time I tried to scale a real-time chat application beyond a single server. Everything worked perfectly in development. Then we deployed two instances behind a load balancer, and users started complaining about missing messages. The culprit was obvious: each application instance had its own in-memory WebSocket broker. They had no idea what the other instance was doing. That night, I learned that simple solutions break fast at scale.
You might ask: Why not just stick with one server? Because real-time features like live notifications, collaborative editing, or financial tickers demand high availability and horizontal scaling. You need an architecture where any instance can talk to any subscriber, regardless of where they are connected. That is exactly what we are building today.
I will walk you through a production-grade WebSocket system using Spring Boot, STOMP, and RabbitMQ. This is not another basic “hello world” tutorial. We will handle authentication, message durability, session management, and cross-instance communication. By the end, you will be able to replace the default in-memory broker with an external, scalable alternative.
Let me start with the fundamental problem. The in-memory broker that Spring Boot provides out of the box is fantastic for demos. It requires zero setup and works well for a single process. But imagine two users connected to different server instances. User A publishes a message on instance 1. The in-memory broker on instance 1 broadcasts it to all its local subscribers. User B, connected to instance 2, never receives it. That is a hard scalability ceiling.
The fix is a shared message broker that all application instances connect to. RabbitMQ fits perfectly because it has a STOMP plugin that turns it into a full STOMP broker. Every instance acts as a client relay: it forwards messages from its WebSocket connections to RabbitMQ, and RabbitMQ routes them to all interested subscribers across any instance. This is the architecture you see in chat platforms, live auctions, and real-time dashboards.
Now you might wonder: Why RabbitMQ and not a simple Redis pub/sub? Redis works for lightweight use cases, but it does not guarantee message delivery or persistence. RabbitMQ offers durable queues, acknowledgments, and flexible routing. For enterprise-grade requirements, it is a solid choice.
Let me show you how to set this up step by step. I assume you have Docker installed. We will start RabbitMQ with the STOMP plugin enabled. Create a file called docker-compose.yml with the following content:
version: '3.8'
services:
rabbitmq:
image: rabbitmq:3.13-management
container_name: rabbitmq-stomp
ports:
- "5672:5672"
- "61613:61613"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
command: >
bash -c "rabbitmq-plugins enable rabbitmq_stomp rabbitmq_web_stomp &&
rabbitmq-server"
Run docker-compose up -d. Verify it works by opening http://localhost:15672 and logging in with guest/guest.
Next, create a Spring Boot project with the required dependencies. You need spring-boot-starter-websocket, spring-boot-starter-amqp, spring-boot-starter-security, and a JWT library. Here is a sample pom.xml snippet:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<!-- plus jjwt-impl and jjwt-jackson runtime -->
Now configure Spring to use RabbitMQ as a STOMP broker relay. Create a configuration class that extends WebSocketMessageBrokerConfigurer. The key parts are enableStompBrokerRelay and the destination prefixes.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// Use RabbitMQ as the external broker
registry.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost("localhost")
.setRelayPort(61613)
.setClientLogin("guest")
.setClientPasscode("guest")
.setSystemLogin("guest")
.setSystemPasscode("guest")
.setHeartbeatSenderInterval(10000)
.setHeartbeatReceiverInterval(10000);
// Application destination prefix
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS(); // fallback for browsers that don't support WebSocket
}
}
Notice that all destinations starting with /topic or /queue are forwarded to RabbitMQ. The /app prefix is for messages sent from the client to the server, which then the server handles and potentially broadcasts.
Now security. You want to authenticate WebSocket connections before the STOMP handshake. A common approach is to pass a JWT token as a query parameter or in the STOMP CONNECT frame headers. Here is how to intercept the handshake using Spring Security’s DefaultHandshakeHandler and a custom interceptor.
First, implement a simple JWT token validator:
@Component
public class JwtTokenProvider {
private final String secret = "my-secret-key-which-is-at-least-256-bits-long";
public String getUserIdFromToken(String token) {
return Jwts.parser()
.verifyWith(new SecretKeySpec(secret.getBytes(), "HmacSHA256"))
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(new SecretKeySpec(secret.getBytes(), "HmacSHA256"))
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException e) {
return false;
}
}
}
Now create a ChannelInterceptor that checks the STOMP CONNECT frame for the token:
@Component
public class AuthChannelInterceptor extends ChannelInterceptor {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String token = accessor.getFirstNativeHeader("Authorization");
if (token == null || token.isEmpty()) {
throw new IllegalArgumentException("Missing token");
}
if (!jwtTokenProvider.validateToken(token)) {
throw new IllegalArgumentException("Invalid token");
}
// Optionally set user identity
String userId = jwtTokenProvider.getUserIdFromToken(token);
accessor.setUser(() -> userId);
}
return message;
}
}
Register this interceptor in the WebSocket configuration:
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(authChannelInterceptor);
}
Now you have a secure WebSocket endpoint that validates JWT on every connection attempt. How do you test this from a JavaScript client? Here is a simple snippet using the STOMP.js library:
const token = "your.jwt.token.here";
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
stompClient.connect(
{'Authorization': token},
function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/messages', function(message) {
console.log('Received: ' + message.body);
});
},
function(error) {
console.error('Connection error: ' + error);
}
);
But what if you need to send a message to a specific user? STOMP supports user queues via the /user prefix. Spring’s SimpMessagingTemplate makes it easy. For example:
@Service
public class NotificationService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
public void sendToUser(String userId, String message) {
messagingTemplate.convertAndSendToUser(userId, "/queue/notifications", message);
}
}
The broker will convert /user/{userId}/queue/notifications to a unique queue name that only the target user’s session subscribes to. This pattern is essential for private messages.
Now, what about handling disconnections and cleaning up? The STOMP protocol defines a DISCONNECT frame, but clients may just close the WebSocket. Spring provides a SessionDisconnectEvent. Listen to it and perform any necessary cleanup:
@Component
public class WebSocketEventListener {
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = headerAccessor.getSessionId();
// Remove session from any local cache, log, etc.
System.out.println("Session disconnected: " + sessionId);
}
}
You might also want to monitor active sessions. Spring Boot Actuator combined with Micrometer can expose metrics about WebSocket sessions. Add the micrometer-registry-prometheus dependency, and then expose your own custom gauge. This helps you understand how many users are connected across all instances.
@Component
public class WebSocketMetrics {
private final MeterRegistry meterRegistry;
private final AtomicInteger activeSessions = new AtomicInteger(0);
public WebSocketMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
Gauge.builder("websocket.active.sessions", activeSessions, AtomicInteger::get)
.description("Number of active WebSocket sessions")
.register(meterRegistry);
}
// Call this from your event listener
public void incrementSessions() { activeSessions.incrementAndGet(); }
public void decrementSessions() { activeSessions.decrementAndGet(); }
}
Now you are ready to scale horizontally. Deploy multiple instances of your Spring Boot application behind a load balancer. Each instance connects to the same RabbitMQ broker. When a user publishes a message, it goes to the broker, and the broker fans it out to all instances that have matching subscriptions. Users see the same messages regardless of which server they hit.
Let me address a common concern: What if RabbitMQ goes down? Your application should handle connection failures gracefully. Spring’s STOMP broker relay has built-in reconnection logic. You can also configure a fallback in-memory broker for critical messages, but for real production systems, you usually set up a RabbitMQ cluster with high availability.
Another question that may come up: How do you handle large payloads like binary files? STOMP supports binary frames via custom headers. However, for large files, you are better off using a separate upload path and sending only the file URL over WebSocket.
I have personally used this architecture in a live auction system where bidding updates had to reach thousands of concurrent users with less than 100ms latency. The combination of Spring Boot, STOMP, and RabbitMQ handled it without a hitch. The key was proper timeout and heartbeat settings to detect stale connections early.
To sum up, you now have a blueprint for a scalable, secure, and observable WebSocket system. Start with a single instance to debug locally, then add more nodes as your user base grows. Always monitor your RabbitMQ queues and connection count. And never underestimate the value of a good disconnection handler.
If you found this guide useful, please like this post, share it with your colleagues who still use in-memory brokers, and leave a comment about your own real-time challenges. Your feedback helps me create better content. Now go build something that moves in real time.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva