WebSockets in System Design: How Real-Time Communication Works
WebSocket is a communication protocol that keeps a single connection open between a client and a server, so both sides can send messages at any time. Unlike normal HTTP, the server does not have to wait for the client to ask. This makes WebSockets the backbone of chat apps, live sports scores, multiplayer games, collaborative editors, trading screens and live dashboards.
This tutorial explains why plain HTTP struggles with real-time data, how a WebSocket connection is opened and used, how it compares to polling and Server-Sent Events, how to build a WebSocket server with Spring Boot, and how to scale WebSockets across many servers.
1. What Are WebSockets?
A WebSocket is a long-lived, full-duplex connection over a single TCP connection. Full-duplex means data can flow in both directions at the same time, like a phone call, rather than taking turns like a walkie-talkie.
The protocol was standardised as RFC 6455 and is supported by every modern browser through the JavaScript WebSocket API. It uses two URL schemes:
ws://for unencrypted connections, normally on port 80.wss://for connections encrypted with TLS, normally on port 443. Always use this in production.
Key properties of WebSockets:
- Persistent: the connection stays open for minutes or hours, until one side closes it.
- Bidirectional: the server can push data the moment something happens.
- Lightweight messages: after the connection opens, each message carries only a few bytes of framing instead of a full set of HTTP headers.
- Web friendly: it starts as an HTTP request, so it works through the same ports, load balancers and proxies as normal web traffic.
2. The Problem with HTTP for Real-Time Data
HTTP follows a strict pattern: the client asks, the server answers, and the exchange is over. The server can never speak first. That is perfect for loading a web page, but awkward when the server has news the client does not know about yet, such as a new chat message.
Before WebSockets, developers worked around this limitation:
- Short polling: the client asks "anything new?" every few seconds. Most answers are "no", which wastes bandwidth, server work and battery, and updates still arrive up to one polling interval late.
- Long polling: the client asks, and the server holds the request open until it has something to send or a timeout passes. Then the client immediately asks again. This is faster, but each message still needs a new HTTP request with full headers.
- Server-Sent Events (SSE): the server keeps one HTTP response open and streams events down it. This works well for server-to-client updates, but the client cannot send messages back on the same channel.
WebSockets solve the problem directly: one connection, kept open, with messages flowing freely both ways.
3. Polling, Long Polling and WebSockets Compared
| Technique | Direction | Latency | Overhead | Best for |
|---|---|---|---|---|
| Short polling | Client asks, server answers | Up to one polling interval | High: many empty requests | Rare updates, very simple setups |
| Long polling | Client asks, server answers when ready | Low | Medium: one full request per message | Fallback when WebSockets are blocked |
| Server-Sent Events | Server to client only | Low | Low | News feeds, notifications, progress updates, streaming AI responses |
| WebSocket | Both directions, any time | Very low | Very low per message | Chat, games, collaboration, live trading |
4. How a WebSocket Connection Is Opened
Every WebSocket connection begins life as a normal HTTP request. The client asks the server to upgrade the connection to the WebSocket protocol. If the server agrees, the same TCP connection is reused, and from that moment it speaks WebSocket instead of HTTP.
GET /ws/chat HTTP/1.1
Host: chat.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: Card00TmooRC6n7D0iaSAQ==
Sec-WebSocket-Version: 13
Origin: https://chat.example.com
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: v/SiwvPtYokDz6vSdUeshRC72bg=
UpgradeandConnectionask to switch protocols.Sec-WebSocket-Keyis a random value created by the browser.Sec-WebSocket-Acceptis calculated by the server from that key (it appends a fixed identifier, hashes the result with SHA-1 and Base64-encodes it). This proves the server really understands WebSockets and is not an ordinary HTTP server replying by accident. It is not a security feature.101 Switching Protocolsconfirms the upgrade. Any other status code means the connection was refused.Origintells the server which website opened the connection, so it can reject unknown sites.
5. Frames, Messages and Control Frames
After the handshake, data travels in small units called frames. A frame has a header of only 2 to 14 bytes, followed by the payload. Compare that with an HTTP request, which often carries hundreds of bytes of headers and cookies every single time.
The main frame types are:
- Text frames: UTF-8 text, commonly JSON such as
{"type":"chat","text":"Hi"}. - Binary frames: raw bytes, useful for images, audio, game state or compact formats like Protocol Buffers.
- Ping and pong frames: heartbeats. One side sends a ping and the other must reply with a pong. They detect dead connections and keep idle connections from being closed by proxies.
- Close frames: start a clean shutdown, with a status code such as
1000(normal closure) or1001(going away, for example a server restart).
A large message can be split across several frames and put back together by the receiver. Frames sent from the browser to the server are also masked with a random key. This is not encryption; it stops badly behaved proxies from mistaking WebSocket data for cacheable HTTP. Real protection comes from wss://.
Note that WebSocket only moves messages. It does not define what they mean. Your application decides the message format, or you can use a sub-protocol such as STOMP that adds destinations and subscriptions on top.
6. The WebSocket Connection Lifecycle
- Connecting: the client sends the upgrade request.
- Open: the server returns 101. Both sides fire an "open" event and can start sending.
- Messaging: messages flow in either direction. Heartbeats keep the connection healthy.
- Closing: one side sends a close frame and the other answers with its own.
- Closed: the TCP connection is released.
Connections can also break without a clean close, for example when a phone loses signal or a laptop goes to sleep. Clients must therefore detect the drop and reconnect automatically, ideally with an increasing delay between attempts so thousands of clients do not hammer the server at once after an outage.
7. Building a WebSocket Server in Spring Boot
Spring Boot provides WebSocket support through the spring-boot-starter-websocket dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
The following handler implements a tiny group chat. It remembers every open session and broadcasts each incoming text message to all of them:
package com.javabytechie.websocket;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
import org.springframework.web.socket.handler.TextWebSocketHandler;
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
// Session id -> session. The decorator makes sending thread-safe.
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.put(session.getId(),
new ConcurrentWebSocketSessionDecorator(session, 5_000, 64 * 1024));
System.out.println("Connected: " + session.getId() + " (online: " + sessions.size() + ")");
}
@Override
protected void handleTextMessage(WebSocketSession sender, TextMessage message) throws IOException {
TextMessage outgoing = new TextMessage("[" + sender.getId() + "] " + message.getPayload());
for (WebSocketSession session : sessions.values()) {
if (session.isOpen()) {
session.sendMessage(outgoing);
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session.getId());
System.out.println("Disconnected: " + session.getId() + " with status " + status.getCode());
}
}
package com.javabytechie.websocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final ChatWebSocketHandler chatHandler;
public WebSocketConfig(ChatWebSocketHandler chatHandler) {
this.chatHandler = chatHandler;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler, "/ws/chat")
.setAllowedOrigins("https://chat.example.com", "http://localhost:3000");
}
}
Connected: 3f1c9a20-77d4-4b8e-9d3f-2a6b1c0e5d11 (online: 1)
Connected: 8b2e6f04-1a5c-4c7d-b0e9-5d4a3f2c1b77 (online: 2)
Disconnected: 3f1c9a20-77d4-4b8e-9d3f-2a6b1c0e5d11 with status 1001
Two details matter here. First, setAllowedOrigins rejects connections opened by other websites. Second, a single WebSocketSession must not be written to by two threads at once, and in a chat many users' messages arrive on different threads. ConcurrentWebSocketSessionDecorator queues the sends safely and drops slow clients that exceed the time or buffer limits.
8. Connecting from the Browser
The browser's built-in WebSocket object needs no library. This client connects, sends a message and reconnects with a growing delay if the connection drops:
let retryDelay = 1000;
function connect() {
const socket = new WebSocket("wss://chat.example.com/ws/chat");
socket.addEventListener("open", () => {
retryDelay = 1000; // reset after a successful connection
socket.send("Hello everyone!");
});
socket.addEventListener("message", (event) => {
console.log("Received:", event.data);
});
socket.addEventListener("close", (event) => {
console.log("Closed with code", event.code, "- retrying in", retryDelay, "ms");
setTimeout(connect, retryDelay);
retryDelay = Math.min(retryDelay * 2, 30000); // back off up to 30 seconds
});
}
connect();
Received: [8b2e6f04-1a5c-4c7d-b0e9-5d4a3f2c1b77] Hello everyone!
9. Using STOMP for Rooms and Topics
Raw WebSockets leave routing to you. For apps with rooms, topics or per-user messages, Spring supports STOMP, a simple messaging protocol that runs over WebSocket. Clients subscribe to destinations such as /topic/rooms/java and send to destinations such as /app/rooms/java/send.
package com.javabytechie.websocket;
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 StompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("https://chat.example.com");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app"); // messages handled by @MessageMapping
registry.enableSimpleBroker("/topic"); // in-memory broker for subscriptions
}
}
package com.javabytechie.websocket;
import java.time.Instant;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class RoomController {
public record ChatMessage(String sender, String text, Instant sentAt) { }
// Client sends to /app/rooms/{roomId}/send, every subscriber of /topic/rooms/{roomId} receives it
@MessageMapping("/rooms/{roomId}/send")
@SendTo("/topic/rooms/{roomId}")
public ChatMessage send(@DestinationVariable String roomId, ChatMessage incoming) {
return new ChatMessage(incoming.sender(), incoming.text(), Instant.now());
}
}
To push data from anywhere in your application, for example when a price changes, inject SimpMessagingTemplate and call messagingTemplate.convertAndSend("/topic/prices", priceUpdate). The simple in-memory broker works on one server. For several servers, Spring can relay STOMP messages to an external broker such as RabbitMQ or ActiveMQ with enableStompBrokerRelay.
10. Scaling WebSockets in System Design
WebSockets change how you design servers, because each connection is stateful and long-lived. A normal HTTP server handles a request and forgets it. A WebSocket server may hold hundreds of thousands of open connections at once, and each user is attached to one particular server.
That creates a new problem: if User A is connected to Server 1 and User B to Server 3, how does A's message reach B? The common answer is a shared publish-subscribe backbone:
Other things to plan for when scaling:
- Load balancer support: the load balancer must allow the HTTP upgrade and keep idle connections open long enough. Nginx, HAProxy, Envoy and cloud application load balancers all support WebSockets, but some need settings such as longer idle timeouts.
- Sticky sessions: once upgraded, a WebSocket stays on the server it reached, so stickiness is only needed if your library falls back to HTTP-based transports such as long polling.
- Connection limits: each open connection uses memory and a file descriptor. Tune OS limits, and prefer non-blocking servers (Netty, Spring WebFlux, or virtual threads) when handling very large numbers of connections.
- Presence tracking: store "who is connected to which server" in a shared store such as Redis, so private messages can be routed to the right server.
- Graceful deploys: when a server shuts down, it should close connections with code
1001so clients reconnect to another server, spread out over time. - Missed messages: while a client is reconnecting, it may miss messages. Persist important messages and let clients ask for everything after the last message ID they saw.
11. Securing WebSockets
- Always use
wss://so messages are encrypted with TLS, exactly like HTTPS. - Check the
Originheader. Browsers send cookies with WebSocket handshakes, so without an origin check a malicious site could open a connection using the victim's session. This attack is called Cross-Site WebSocket Hijacking. - Authenticate during the handshake. Use the existing session cookie, or a short-lived token. Avoid putting long-lived tokens in the URL, because URLs end up in logs.
- Authorise every message, not just the connection. A user allowed into room A must not be able to send to room B just by changing the destination.
- Validate and limit input. Set a maximum message size, rate-limit messages per connection, and treat every payload as untrusted.
- Expire long sessions. A connection can outlive the login token that opened it, so re-check permissions periodically or close connections when a token expires.
12. WebSocket vs HTTP: Comparison Table
| Aspect | HTTP | WebSocket |
|---|---|---|
| Communication model | Request-response; client always starts | Full-duplex; either side can send any time |
| Connection | Short-lived or reused for many requests | One persistent connection per client |
| Server push | Not possible without workarounds | Built in |
| Per-message overhead | Full headers on every request | 2 to 14 bytes of framing |
| State | Stateless; easy to scale horizontally | Stateful; needs pub/sub to scale |
| Caching | Supported by browsers, proxies and CDNs | Not applicable |
| URL schemes | http://, https:// | ws://, wss:// |
| Message meaning | Methods, status codes, standard semantics | Defined by your application or a sub-protocol |
| Best for | CRUD APIs, page loads, file downloads | Chat, games, live collaboration, streaming updates |
13. When to Use (and Not Use) WebSockets
Good fits:
- Chat and messaging apps, including typing indicators and read receipts.
- Multiplayer games and real-time collaborative tools such as shared whiteboards and documents.
- Live financial prices, sports scores and auction bids.
- Live location tracking, such as a delivery driver on a map.
- Operational dashboards that update every second.
Poor fits:
- Ordinary CRUD APIs where the client asks and waits for one answer. REST is simpler, cacheable and stateless.
- One-way server updates such as notifications or progress bars. Server-Sent Events are simpler and work over plain HTTP.
- Updates that happen once every few minutes. Polling or push notifications cost less than keeping millions of connections open.
14. Common Mistakes to Avoid
- No reconnection logic. Networks drop all the time, especially on mobile. Without automatic reconnect, users silently stop receiving updates.
- No heartbeats. Idle connections are often closed by proxies after 60 seconds or so. Send pings, or application-level heartbeats, more often than that.
- Keeping state only in server memory. When a server restarts, everything it held disappears. Store important data in a database or broker.
- Skipping origin checks and authorisation. This opens the door to hijacked connections and users reading other people's rooms.
- Writing to one session from many threads. This causes errors or garbled messages. Use a thread-safe wrapper or a single writer per session.
- Using WebSockets for everything. Request-response operations are easier to build, test, cache and monitor over HTTP.
15. Best Practices
- Use
wss://, validate theOriginheader, and authenticate during the handshake. - Define a clear message format, for example JSON with a
typefield and a version, or use STOMP. - Implement heartbeats and client reconnection with exponential backoff and random jitter.
- Use a pub/sub broker such as Redis, Kafka or RabbitMQ to fan out messages across servers.
- Limit message size and message rate per connection, and drop slow consumers instead of buffering forever.
- Monitor open connections, messages per second, reconnect rates and delivery latency.
- Give each message an ID so clients can detect gaps and request missed messages after reconnecting.
- Keep REST for regular operations and use WebSockets only for the real-time parts of your product.
16. Summary
- A WebSocket is a persistent, full-duplex connection that lets the client and server send messages at any time.
- It begins with an HTTP upgrade request and a
101 Switching Protocolsresponse, then carries small frames over the same TCP connection. - It beats polling and long polling for latency and overhead, while Server-Sent Events remain a simpler option for one-way updates.
- Spring Boot supports raw WebSocket handlers and STOMP messaging for rooms and topics.
- Because connections are stateful, scaling needs a pub/sub broker, heartbeats, reconnection and careful security.
Use WebSockets where instant, two-way updates are part of the product experience, and keep simple request-response work on HTTP. Open the FAQ tab for answers to common questions about WebSockets.
Frequently Asked Questions (FAQ)
Q1. What is a WebSocket in simple words?
A WebSocket is a connection between a browser (or app) and a server that stays open, so either side can send messages at any moment. It is like keeping a phone call open instead of sending a new letter every time you have something to say.
Q2. How is WebSocket different from HTTP?
HTTP is request-response: the client asks, the server answers, and the server can never speak first. WebSocket keeps one connection open and lets both sides send messages freely, with very little overhead per message. HTTP is stateless and cacheable; WebSocket connections are stateful and long-lived.
Q3. Does WebSocket use HTTP?
Only at the start. A WebSocket connection begins with an HTTP request containing Upgrade: websocket. When the server replies with 101 Switching Protocols, the same TCP connection stops speaking HTTP and switches to the WebSocket protocol.
Q4. What is the difference between ws:// and wss://?
ws:// is an unencrypted WebSocket connection, and wss:// is a WebSocket connection encrypted with TLS, just like HTTPS. Always use wss:// in production to protect data and to pass reliably through proxies and firewalls.
Q5. What port does WebSocket use?
WebSockets use the same ports as the web: 80 for ws:// and 443 for wss://. Because they share these ports with HTTP and HTTPS, they usually work through firewalls without special rules.
Q6. What is the difference between WebSockets and long polling?
With long polling, the client sends a request and the server holds it until data is available, then the client must send a new request for the next update. Each message costs a full HTTP request. With WebSockets, one connection stays open and messages flow in both directions with only a few bytes of overhead each.
Q7. When should I use Server-Sent Events instead of WebSockets?
Use Server-Sent Events (SSE) when only the server needs to send updates, such as notifications, live feeds, progress bars or streamed text. SSE runs over normal HTTP, reconnects automatically and is simpler to operate. Choose WebSockets when the client also needs to send frequent messages, such as in chat or games.
Q8. What is the WebSocket handshake?
The handshake is the HTTP exchange that opens a WebSocket. The client sends headers including Upgrade: websocket and a random Sec-WebSocket-Key. The server replies with 101 Switching Protocols and a Sec-WebSocket-Accept value calculated from the key, confirming it supports WebSockets.
Q9. What are ping and pong frames?
Ping and pong are control frames used as heartbeats. One side sends a ping, and the other must answer with a pong. They help detect connections that died without closing properly and prevent idle connections from being cut off by proxies and load balancers.
Q10. Are WebSockets stateful?
Yes. Each connection is tied to one server for its whole life, and that server keeps information about it in memory. This makes horizontal scaling harder than with stateless HTTP, so systems use shared pub/sub brokers and external stores to coordinate between servers.
Q11. How do you scale WebSocket servers?
- Run several WebSocket servers behind a load balancer that supports the upgrade.
- Use a pub/sub broker such as Redis, Kafka or RabbitMQ so messages reach users connected to other servers.
- Store presence information (which user is on which server) in a shared store.
- Use non-blocking servers and tune OS limits for many open connections.
- Make clients reconnect with backoff so restarts do not cause a flood.
Q12. Do WebSockets need sticky sessions?
Not for the WebSocket connection itself: once upgraded, it stays on the same server automatically. Sticky sessions are needed only when a library falls back to HTTP transports like long polling, where several separate requests must reach the same server.
Q13. What is STOMP in Spring WebSocket?
STOMP (Simple Text Oriented Messaging Protocol) is a small messaging protocol that runs on top of WebSocket. It adds commands such as SUBSCRIBE and SEND and the idea of destinations. Spring uses it so you can write @MessageMapping controller methods and broadcast to topics such as /topic/rooms/java.
Q14. How do I secure a WebSocket connection?
Use wss://, check the Origin header against an allow list, authenticate the user during the handshake, authorise each message or subscription, limit message size and rate, and close connections when the user's session or token expires.
Q15. What is Cross-Site WebSocket Hijacking?
It is an attack where a malicious website opens a WebSocket to your server from the victim's browser. Because the browser sends the victim's cookies, the server may treat the connection as the logged-in user. Checking the Origin header and using tokens or CSRF protection during the handshake prevents it.
Q16. How many WebSocket connections can one server handle?
It depends on memory, CPU, message rate and server software. A well-tuned non-blocking server can hold tens or hundreds of thousands of mostly idle connections. Busy connections that send many messages need more resources. Load test with realistic traffic to find your limit.
Q17. What happens to a WebSocket when the network drops?
The connection may die without either side being told immediately. Heartbeats detect it, and the client should reconnect automatically with an increasing delay. After reconnecting, the client can request any messages it missed, using the last message ID it received.
Q18. Can WebSockets send binary data?
Yes. WebSocket supports both text frames (UTF-8) and binary frames. Binary frames are useful for images, audio, game state and compact encodings such as Protocol Buffers or MessagePack.
Q19. Can a CDN or load balancer handle WebSockets?
Most modern load balancers and many CDNs support WebSockets, including Nginx, HAProxy, Envoy and major cloud load balancers. You may need to enable upgrade handling and raise idle timeouts so long-lived connections are not closed too early.
Q20. What are common real-world uses of WebSockets?
Chat and messaging, multiplayer games, collaborative editing, live sports scores, stock and crypto price tickers, live auctions, delivery and ride tracking, customer support widgets and real-time monitoring dashboards.