What are WebSockets?
WebSocket is a communication protocol that provides full-duplex (two-way) communication channels over a single, long-lived TCP connection. It represents a fundamental shift from HTTP's request-response model to enable true real-time communication on the web.
WebSockets were standardized in 2011 (RFC 6455) to solve a problem that HTTP couldn't address well: server-initiated communication. With HTTP, the server can only respond to client requests—it can never proactively send data to the client. This limitation made building real-time applications like chat, gaming, and live dashboards difficult and inefficient.
The Problem WebSockets Solve
Before WebSockets, developers used workarounds to simulate real-time communication:
- Polling: Client asks "Any updates?" every few seconds. Wasteful—most requests return nothing new.
- Long Polling: Client asks, server holds the connection open until there's data. Better, but still creates overhead of repeatedly establishing connections.
- Server-Sent Events (SSE): Server can push data, but only server-to-client. Client still uses HTTP for sending.
WebSockets provide a clean solution: a single persistent connection where both sides can send messages at any time.
Real-World Examples
You encounter WebSockets frequently, even if you don't realize it:
- Chat applications: Slack, Discord, WhatsApp Web—messages appear instantly without refreshing
- Collaborative editing: Google Docs, Figma—multiple users see each other's changes in real-time
- Live sports/stock tickers: Scores and prices update continuously
- Online gaming: Player movements and actions synchronized instantly
- Notifications: Real-time alerts that appear without polling
- Live dashboards: Metrics that update as data changes
Key Difference from HTTP:
- HTTP: Client asks, server responds. The server can NEVER initiate communication. Each request requires a new connection setup (in HTTP/1.1 without keep-alive).
- WebSocket: Both parties can send messages anytime over a persistent connection. The server can push data to clients without being asked. Low overhead per message.
// HTTP: Request-Response (Half-duplex)
Client ──Request──> Server
Client <──Response── Server
Client ──Request──> Server
Client <──Response── Server
// Client must always initiate!
// WebSocket: Full-duplex
Client <────────────> Server
↑ ↑
│ └── Server can push anytime
└─────────────── Client can send anytime
// Perfect for: Chat, gaming, live updates, notifications
WebSocket vs HTTP
| Aspect | HTTP | WebSocket |
|---|---|---|
| Communication | Request-Response | Bidirectional |
| Connection | New connection per request | Persistent connection |
| Initiation | Client only | Either side |
| Overhead | Headers on every request | Minimal frame overhead |
| Latency | Higher (connection setup) | Lower (persistent) |
| Use Case | CRUD operations, static content | Real-time updates, streaming |
How WebSockets Work
// WebSocket Lifecycle
┌──────────────────────────────────────────────────────────────────┐
│ 1. HANDSHAKE (HTTP Upgrade) │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Client Request: │
│ GET /chat HTTP/1.1 │
│ Host: server.example.com │
│ Upgrade: websocket ← Request protocol upgrade │
│ Connection: Upgrade │
│ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== │
│ Sec-WebSocket-Version: 13 │
│ │
│ Server Response: │
│ HTTP/1.1 101 Switching Protocols │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= │
│ │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ 2. CONNECTION ESTABLISHED │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ WebSocket ┌─────────┐ │
│ │ Client │ <═══════════════════════════> │ Server │ │
│ └─────────┘ Bidirectional └─────────┘ │
│ │
│ - Both can send messages anytime │
│ - Low overhead per message (2-14 bytes) │
│ - Connection stays open │
│ │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ 3. DATA EXCHANGE │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Client: {"type": "message", "text": "Hello!"} │
│ Server: {"type": "message", "from": "john", "text": "Hello!"} │
│ Server: {"type": "notification", "count": 5} │
│ Client: {"type": "typing", "user": "jane"} │
│ │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ 4. CONNECTION CLOSE │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Either side sends close frame │
│ Other side acknowledges │
│ TCP connection terminates │
│ │
└──────────────────────────────────────────────────────────────────┘
WebSocket in JavaScript (Client)
// Basic WebSocket connection
const socket = new WebSocket('wss://api.example.com/ws');
// Connection opened
socket.onopen = function(event) {
console.log('Connected to WebSocket!');
// Send a message
socket.send(JSON.stringify({
type: 'join',
room: 'general'
}));
};
// Listen for messages
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Received:', data);
if (data.type === 'message') {
displayMessage(data);
} else if (data.type === 'notification') {
showNotification(data);
}
};
// Connection closed
socket.onclose = function(event) {
if (event.wasClean) {
console.log('Connection closed cleanly');
} else {
console.log('Connection died');
}
console.log('Code:', event.code, 'Reason:', event.reason);
};
// Connection error
socket.onerror = function(error) {
console.error('WebSocket error:', error);
};
// Send message function
function sendMessage(text) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: 'message',
text: text,
timestamp: Date.now()
}));
}
}
// Close connection
function disconnect() {
socket.close(1000, 'User logged out');
}
WebSocket with Reconnection
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.connect();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('Connected');
this.reconnectAttempts = 0; // Reset on successful connection
};
this.socket.onclose = (event) => {
if (!event.wasClean) {
this.reconnect();
}
};
this.socket.onerror = (error) => {
console.error('WebSocket error', error);
};
this.socket.onmessage = (event) => {
this.onMessage(JSON.parse(event.data));
};
}
reconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * this.reconnectAttempts;
console.log(`Reconnecting in ${delay}ms... (attempt ${this.reconnectAttempts})`);
setTimeout(() => this.connect(), delay);
} else {
console.error('Max reconnection attempts reached');
}
}
send(data) {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
}
}
onMessage(data) {
// Override this in subclass or set externally
console.log('Received:', data);
}
}
WebSocket in Spring Boot (Server)
Dependencies
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
Basic WebSocket Handler
// 1. WebSocket Configuration
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private ChatWebSocketHandler chatHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler, "/ws/chat")
.setAllowedOrigins("https://myapp.com"); // CORS for WebSocket
}
}
// 2. WebSocket Handler
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
private final Set<WebSocketSession> sessions =
ConcurrentHashMap.newKeySet();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session);
System.out.println("New connection: " + session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session,
TextMessage message) throws Exception {
String payload = message.getPayload();
System.out.println("Received: " + payload);
// Parse the message
ObjectMapper mapper = new ObjectMapper();
ChatMessage chatMessage = mapper.readValue(payload, ChatMessage.class);
// Broadcast to all connected clients
for (WebSocketSession s : sessions) {
if (s.isOpen()) {
s.sendMessage(new TextMessage(payload));
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session,
CloseStatus status) {
sessions.remove(session);
System.out.println("Connection closed: " + session.getId());
}
@Override
public void handleTransportError(WebSocketSession session,
Throwable exception) {
System.err.println("Error: " + exception.getMessage());
sessions.remove(session);
}
// Send message to specific session
public void sendToSession(String sessionId, String message) {
sessions.stream()
.filter(s -> s.getId().equals(sessionId))
.findFirst()
.ifPresent(s -> {
try {
s.sendMessage(new TextMessage(message));
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
STOMP over WebSocket (Recommended)
STOMP (Simple Text Oriented Messaging Protocol) is a messaging protocol that runs over WebSocket, providing features like message routing, subscriptions, and better organization.
Server Configuration
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// Enable simple in-memory message broker
// Client subscribes to /topic/* or /queue/*
config.enableSimpleBroker("/topic", "/queue");
// Prefix for messages FROM client TO server
config.setApplicationDestinationPrefixes("/app");
// Prefix for user-specific messages
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// WebSocket endpoint - clients connect here
registry.addEndpoint("/ws")
.setAllowedOrigins("https://myapp.com")
.withSockJS(); // Fallback for old browsers
}
}
Controller for Handling Messages
@Controller
public class ChatController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
// Handle messages sent to /app/chat
@MessageMapping("/chat")
@SendTo("/topic/messages") // Broadcast to all subscribers
public ChatMessage handleMessage(ChatMessage message,
Principal principal) {
message.setSender(principal.getName());
message.setTimestamp(LocalDateTime.now());
return message; // Automatically sent to /topic/messages
}
// Send to specific user
@MessageMapping("/private")
public void handlePrivateMessage(PrivateMessage message,
Principal principal) {
message.setSender(principal.getName());
// Send to specific user's queue
messagingTemplate.convertAndSendToUser(
message.getRecipient(),
"/queue/private",
message
);
}
// Send from anywhere in the application
public void sendNotification(String userId, Notification notification) {
messagingTemplate.convertAndSendToUser(
userId,
"/queue/notifications",
notification
);
}
// Broadcast to all
public void broadcastAnnouncement(Announcement announcement) {
messagingTemplate.convertAndSend("/topic/announcements", announcement);
}
}
JavaScript Client with STOMP
// Using stompjs library
// npm install @stomp/stompjs sockjs-client
import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';
const client = new Client({
// Use SockJS for fallback support
webSocketFactory: () => new SockJS('https://api.myapp.com/ws'),
// Or native WebSocket
// brokerURL: 'wss://api.myapp.com/ws',
reconnectDelay: 5000,
onConnect: (frame) => {
console.log('Connected!');
// Subscribe to public topic
client.subscribe('/topic/messages', (message) => {
const chatMessage = JSON.parse(message.body);
displayMessage(chatMessage);
});
// Subscribe to user-specific queue
client.subscribe('/user/queue/private', (message) => {
const privateMessage = JSON.parse(message.body);
displayPrivateMessage(privateMessage);
});
// Subscribe to notifications
client.subscribe('/user/queue/notifications', (message) => {
const notification = JSON.parse(message.body);
showNotification(notification);
});
},
onDisconnect: () => {
console.log('Disconnected');
},
onStompError: (frame) => {
console.error('STOMP error:', frame.headers['message']);
}
});
// Activate connection
client.activate();
// Send message to /app/chat
function sendMessage(text) {
client.publish({
destination: '/app/chat',
body: JSON.stringify({
type: 'message',
text: text
})
});
}
// Send private message
function sendPrivateMessage(recipient, text) {
client.publish({
destination: '/app/private',
body: JSON.stringify({
recipient: recipient,
text: text
})
});
}
// Disconnect
function disconnect() {
client.deactivate();
}
WebSocket Authentication
// 1. Handshake Interceptor - Authenticate during connection
@Component
public class AuthHandshakeInterceptor implements HandshakeInterceptor {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler handler,
Map<String, Object> attributes) {
// Get token from query parameter or header
String token = extractToken(request);
if (token != null && tokenProvider.validateToken(token)) {
String userId = tokenProvider.getUserId(token);
attributes.put("userId", userId);
return true; // Allow connection
}
return false; // Reject connection
}
private String extractToken(ServerHttpRequest request) {
// From query parameter: /ws?token=xxx
String query = request.getURI().getQuery();
if (query != null && query.contains("token=")) {
return query.split("token=")[1].split("&")[0];
}
return null;
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler handler,
Exception ex) { }
}
// 2. Register the interceptor
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private AuthHandshakeInterceptor authInterceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.addInterceptors(authInterceptor)
.setAllowedOrigins("*")
.withSockJS();
}
}
// 3. Channel Interceptor - Authenticate STOMP messages
@Component
public class AuthChannelInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
StompHeaderAccessor.wrap(message);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String token = accessor.getFirstNativeHeader("Authorization");
// Validate token and set principal
}
return message;
}
}
Real-World Example: Live Chat
// Message DTOs
public record ChatMessage(
String id,
String sender,
String content,
String room,
LocalDateTime timestamp
) {}
public record JoinMessage(
String username,
String room
) {}
// Chat Controller
@Controller
public class ChatController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
private final Map<String, Set<String>> roomUsers = new ConcurrentHashMap<>();
@MessageMapping("/chat.join")
public void joinRoom(JoinMessage joinMessage, SimpMessageHeaderAccessor headerAccessor) {
String sessionId = headerAccessor.getSessionId();
String room = joinMessage.room();
String username = joinMessage.username();
// Store user in room
roomUsers.computeIfAbsent(room, k -> ConcurrentHashMap.newKeySet()).add(username);
// Store in session attributes
headerAccessor.getSessionAttributes().put("username", username);
headerAccessor.getSessionAttributes().put("room", room);
// Notify room that user joined
messagingTemplate.convertAndSend(
"/topic/chat/" + room,
new ChatMessage(
UUID.randomUUID().toString(),
"SYSTEM",
username + " joined the chat",
room,
LocalDateTime.now()
)
);
// Send updated user list
messagingTemplate.convertAndSend(
"/topic/chat/" + room + "/users",
roomUsers.get(room)
);
}
@MessageMapping("/chat.send")
public void sendMessage(ChatMessage message) {
ChatMessage enrichedMessage = new ChatMessage(
UUID.randomUUID().toString(),
message.sender(),
message.content(),
message.room(),
LocalDateTime.now()
);
messagingTemplate.convertAndSend(
"/topic/chat/" + message.room(),
enrichedMessage
);
}
// Handle disconnection
@EventListener
public void handleSessionDisconnect(SessionDisconnectEvent event) {
SimpMessageHeaderAccessor headerAccessor =
SimpMessageHeaderAccessor.wrap(event.getMessage());
String username = (String) headerAccessor.getSessionAttributes().get("username");
String room = (String) headerAccessor.getSessionAttributes().get("room");
if (username != null && room != null) {
roomUsers.getOrDefault(room, Set.of()).remove(username);
messagingTemplate.convertAndSend(
"/topic/chat/" + room,
new ChatMessage(
UUID.randomUUID().toString(),
"SYSTEM",
username + " left the chat",
room,
LocalDateTime.now()
)
);
}
}
}
WebSocket vs Alternatives
| Technology | Direction | Connection | Use Case |
|---|---|---|---|
| WebSocket | Bidirectional | Persistent | Chat, gaming, collaboration |
| Server-Sent Events | Server → Client | Persistent | News feeds, notifications |
| Long Polling | Simulated push | Repeated requests | Legacy support |
| HTTP/2 Push | Server → Client | Request-based | Resource preloading |
- Real-time bidirectional communication needed
- Low latency is critical
- Frequent small messages
- Gaming, chat, live collaboration
When NOT to use: Simple one-way notifications (use SSE), infrequent updates (use polling), RESTful CRUD operations (use HTTP)
Best Practices
DO:
- Use WSS (WebSocket Secure) - Always encrypt in production
- Implement heartbeat/ping-pong - Detect dead connections
- Handle reconnection - Network can be unreliable
- Use STOMP for complex apps - Better message routing
- Authenticate connections - Validate tokens during handshake
- Limit message size - Prevent DoS attacks
- Use message queues for scale - RabbitMQ, Redis Pub/Sub
DON'T:
- Don't use WebSocket for everything - REST is fine for CRUD
- Don't trust client messages - Always validate on server
- Don't forget error handling - Connections can fail
- Don't ignore backpressure - Handle slow consumers
- Don't store sensitive data in messages - Use references
Summary
- WebSocket: Full-duplex communication protocol over TCP
- Handshake: HTTP upgrade to WebSocket protocol
- Persistent: Connection stays open for continuous communication
- STOMP: Messaging protocol that runs over WebSocket
- Topics: Pub/sub for broadcasting to multiple clients
- Queues: Point-to-point for user-specific messages
- Use cases: Chat, gaming, notifications, live updates