00:00

WebSockets in System Design: How Real-Time Communication Works

Last updated: 15 min read

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

Short polling, long polling and WebSockets compared Three small sequence diagrams. Short polling: the client repeatedly asks for new data and usually hears no. Long polling: the client asks once and the server waits until data is ready before replying. WebSocket: after one upgrade request and a 101 response, the server pushes several messages and the client sends one, all on the same connection. SHORT POLLING LONG POLLING WEBSOCKET Client Server new data? no new data? no new data? yes, here many wasted requests Client Server new data? server waits… here it is new data? (again) request held until data Client Server upgrade 101 switching push push send push one connection, both ways
Figure 1: Short polling wastes requests, long polling holds requests open, and a WebSocket keeps one connection open for messages in both directions.
TechniqueDirectionLatencyOverheadBest for
Short pollingClient asks, server answersUp to one polling intervalHigh: many empty requestsRare updates, very simple setups
Long pollingClient asks, server answers when readyLowMedium: one full request per messageFallback when WebSockets are blocked
Server-Sent EventsServer to client onlyLowLowNews feeds, notifications, progress updates, streaming AI responses
WebSocketBoth directions, any timeVery lowVery low per messageChat, 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.

Client request
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
Server response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: v/SiwvPtYokDz6vSdUeshRC72bg=
  • Upgrade and Connection ask to switch protocols.
  • Sec-WebSocket-Key is a random value created by the browser.
  • Sec-WebSocket-Accept is 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 Protocols confirms the upgrade. Any other status code means the connection was refused.
  • Origin tells the server which website opened the connection, so it can reject unknown sites.
The life of a WebSocket connection A sequence diagram between a browser and a server. The browser sends an HTTP GET with Upgrade websocket, the server answers 101 Switching Protocols. Then a wide band shows WebSocket messages flowing in both directions over the same connection, with periodic ping and pong frames. Finally each side sends a close frame. Browser Server HTTP GET · Upgrade: websocket HTTP 101 Switching Protocols WebSocket messages, both directions text or binary frames · any time · same TCP connection ping / pong frames keep it alive Close frame (code 1000) Close frame echoed
Figure 2: A WebSocket starts as one HTTP request, switches protocol with a 101 response, carries messages both ways for as long as needed, and ends with a close handshake.

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) or 1001 (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

  1. Connecting: the client sends the upgrade request.
  2. Open: the server returns 101. Both sides fire an "open" event and can start sending.
  3. Messaging: messages flow in either direction. Heartbeats keep the connection healthy.
  4. Closing: one side sends a close frame and the other answers with its own.
  5. 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:

pom.xml
<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:

ChatWebSocketHandler.java
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());
    }
}
WebSocketConfig.java
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");
    }
}
Console output
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:

chat.js
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();
Browser console
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.

StompConfig.java
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
    }
}
RoomController.java
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:

Scaling WebSocket servers with a pub/sub broker Three users connect through a load balancer to three WebSocket servers. Each server is connected to a shared publish-subscribe broker such as Redis or Kafka. A message received by one server is published to the broker and delivered by all servers to their own connected users. User A User B User C Load Balancer WS Server 1holds A's connection WS Server 2holds B's connection WS Server 3holds C's connection Pub/Sub Broker Redis · Kafka · RabbitMQ Server 1 publishes A's message · all servers receive it · each delivers to its own users
Figure 3: Each user stays connected to one WebSocket server. A shared pub/sub broker carries messages between servers so every user receives them.

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 1001 so 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 Origin header. 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

AspectHTTPWebSocket
Communication modelRequest-response; client always startsFull-duplex; either side can send any time
ConnectionShort-lived or reused for many requestsOne persistent connection per client
Server pushNot possible without workaroundsBuilt in
Per-message overheadFull headers on every request2 to 14 bytes of framing
StateStateless; easy to scale horizontallyStateful; needs pub/sub to scale
CachingSupported by browsers, proxies and CDNsNot applicable
URL schemeshttp://, https://ws://, wss://
Message meaningMethods, status codes, standard semanticsDefined by your application or a sub-protocol
Best forCRUD APIs, page loads, file downloadsChat, 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 the Origin header, and authenticate during the handshake.
  • Define a clear message format, for example JSON with a type field 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 Protocols response, 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.