What Is WebSocket?
WebSocket is a communication protocol that provides a persistent, bidirectional connection between a client and a server. Unlike traditional HTTP requests, where the client initiates each exchange, WebSocket allows both sides to send messages independently over the same connection. It is commonly used for chat applications, multiplayer games, collaborative editing, trading dashboards, and real-time notifications.
WebSocket eliminates the need to repeatedly establish HTTP requests for every update, but persistent connections introduce challenges that ordinary REST APIs rarely face: connection state, authentication, heartbeats, backpressure, message delivery, and scaling across multiple servers. Understanding these trade-offs is essential before choosing WebSocket for a production system.
Table of Contents
- How WebSocket Works
- WebSocket Protocol and Connection Lifecycle
- WebSocket vs. HTTP, SSE, and Long Polling
- Implementing WebSocket with FastAPI
- WebSocket Security and Authentication
- Production Design Example
- Ready-to-Use Example
- Common Mistakes
- Production Checklist
- Frequently Asked Questions
- Conclusion
How WebSocket Works
A WebSocket connection starts with an HTTP-based handshake. After the connection is established, communication switches to WebSocket frames rather than ordinary HTTP request-response messages.
The client and server can then exchange text or binary messages without creating a new HTTP request for each message. The connection remains active until one side closes it or the underlying network connection fails.
WebSocket Handshake
In the widely used HTTP/1.1 handshake, the client sends an HTTP GET request asking the server to upgrade the connection to the WebSocket protocol.
GET /ws/chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
If the server accepts the request, it returns HTTP 101 Switching Protocols with the appropriate handshake headers.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The Sec-WebSocket-Accept value is derived from the client's key using a defined SHA-1 and Base64 transformation. It confirms that the server understands the WebSocket handshake; it is not an authentication mechanism.
After this response, the client and server exchange WebSocket frames over the established connection.
WebSocket can also be established over HTTP/2 using extended CONNECT, where supported. The traditional HTTP/1.1 Upgrade handshake remains important because it is widely deployed and supported by reverse proxies.
Full-Duplex Communication
WebSocket is full-duplex: both the client and server can send messages independently at the same time.
Consider a chat application. A browser can send a message while simultaneously receiving messages from other users. Neither operation requires waiting for the other side to initiate a new request.
This differs from Server-Sent Events, where the established stream primarily carries data from server to client, and the client sends commands through separate HTTP requests.
WebSocket is particularly useful when both communication directions are active and low latency matters.
WebSocket Protocol and Connection Lifecycle
WebSocket operates at the application layer over a reliable transport, typically TCP with optional TLS. Its message framing allows applications to exchange text and binary data while the connection remains open.
The protocol defines data frames and control frames for operations such as closing a connection or checking whether the peer is responsive.
WebSocket Frames and Messages
A WebSocket message may consist of one or more frames. The protocol supports text messages, binary messages, and fragmented messages.
Text frames contain UTF-8 data, while binary frames can carry arbitrary bytes. This makes WebSocket suitable for both JSON-based application messages and binary protocols.
Client-to-server frames must be masked according to the WebSocket protocol. Server-to-client frames are not masked. WebSocket libraries handle this automatically, so application code should not manually construct frame headers in normal implementations.
For example, a chat service may use JSON messages:
{
"type": "chat.message",
"message_id": "msg_1024",
"room_id": "room_42",
"text": "Hello!",
"sent_at": "2026-10-10T14:30:00Z"
}
The WebSocket protocol transports the message, but the application defines the meaning of fields such as type, room_id, and message_id.
Ping, Pong, and Heartbeats
WebSocket includes Ping and Pong control frames. A peer can send Ping, and the other side responds with Pong to indicate that the connection is still responsive.
These frames are useful for detecting dead connections and keeping otherwise idle connections active through some network infrastructure.
However, WebSocket Ping/Pong does not automatically establish application health. A server might respond to protocol-level Ping while its database or message broker is unavailable.
Applications may therefore need separate readiness checks and business-level health monitoring.
Browser JavaScript does not expose a direct API for sending protocol-level Ping frames. Browsers handle protocol details internally, while applications can implement their own heartbeat messages if required.
Connection Closure and Failure Detection
WebSocket supports an explicit closing handshake using Close control frames. Common close codes include:
| Code | Meaning | Typical Scenario |
|---|---|---|
| 1000 | Normal closure | Client intentionally disconnects |
| 1001 | Going away | Server shutdown or page navigation |
| 1002 | Protocol error | Invalid WebSocket framing |
| 1008 | Policy violation | Application rejects an unauthorized operation |
| 1011 | Internal server error | Unexpected server-side failure |
Not every disconnection includes a clean closing handshake. Network failures can terminate the underlying connection without either peer receiving a Close frame.
Clients should distinguish intentional shutdown from unexpected disconnection and implement reconnection where appropriate.
WebSocket vs. HTTP, SSE, and Long Polling
WebSocket is not a universal replacement for HTTP. Traditional request-response APIs remain simpler for operations such as fetching resources, submitting forms, and creating records.
The right communication mechanism depends on message frequency, communication direction, infrastructure support, and reliability requirements.
| Characteristic | REST over HTTP | WebSocket | SSE | Long Polling |
|---|---|---|---|---|
| Communication | Request-response | Bidirectional | Server to client | Client request, delayed server response |
| Connection model | Independent requests | Persistent connection | Persistent HTTP response | Repeated held requests |
| Binary data | Supported | Supported | Text-based | Supported through HTTP |
| Automatic browser reconnection | Not applicable | No | Yes | No |
| Best fit | CRUD APIs | Interactive applications | Live server updates | Occasional event notifications |
When to Use WebSocket
WebSocket is a strong choice when an application requires frequent bidirectional messages and benefits from keeping one connection open.
Examples include chat, collaborative document editing, multiplayer games, remote terminals, and interactive trading interfaces.
For occasional notifications, SSE or long polling may be easier to operate. For ordinary resource operations, REST APIs usually remain preferable.
WebSocket and REST can coexist in the same application. A chat platform might use REST for loading conversation history and WebSocket for exchanging new messages.
Implementing WebSocket with FastAPI
FastAPI supports WebSocket endpoints through Starlette. An endpoint accepts the connection and then asynchronously receives and sends messages.
The application must explicitly manage connection lifetime and handle disconnections. Unlike a typical REST request, a WebSocket handler may remain active for minutes or hours.
Minimal WebSocket Server
The following example implements a simple echo server. Every text message received from the client is sent back over the same connection.
from fastapi import FastAPI, WebSocket
from starlette.websockets import WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
message = await websocket.receive_text()
await websocket.send_text(
f"Echo: {message}"
)
except WebSocketDisconnect:
pass
The receive_text() call waits asynchronously for incoming messages. While waiting, the event loop can continue processing other requests and connections.
When the client disconnects, Starlette raises WebSocketDisconnect, allowing the handler to exit.
Browser WebSocket Client
The browser provides a native WebSocket interface for opening connections and exchanging messages.
const socket = new WebSocket(
"ws://localhost:8000/ws"
);
socket.addEventListener("open", () => {
console.log("Connected");
socket.send("Hello from the browser");
});
socket.addEventListener("message", (event) => {
console.log("Received:", event.data);
});
socket.addEventListener("close", (event) => {
console.log(
"Disconnected",
event.code,
event.reason
);
});
socket.addEventListener("error", () => {
console.error("WebSocket error");
});
For production websites served over HTTPS, use wss:// to establish a TLS-protected WebSocket connection.
The browser's WebSocket API does not automatically reconnect after unexpected disconnections. Applications must implement reconnection logic themselves.
Message Validation and Protocol Design
A WebSocket connection can carry many kinds of application messages. Rather than sending arbitrary JSON structures, define explicit message types and validate each incoming payload.
For example, a chat application might support chat.send, chat.typing, room.join, and room.leave.
Message validation should include type checks, payload-size limits, authorization, and appropriate handling of unsupported message versions.
Do not assume that a successfully authenticated connection authorizes every subsequent operation. A user connected to one chat room should not automatically be allowed to publish messages to every other room.
WebSocket Security and Authentication
WebSocket connections create a long-lived communication channel that can carry sensitive information and state-changing commands. Security must cover both connection establishment and individual messages.
Authentication During the Handshake
WebSocket authentication can use session cookies, short-lived connection tokens, or authentication performed through an application-defined message after connection establishment.
Browser WebSocket clients cannot set arbitrary HTTP headers during the handshake. This limits some authentication patterns that work naturally with ordinary HTTP clients.
For browser applications, cookie-based authentication is common, but the server must validate the Origin header to reduce cross-site WebSocket hijacking risks.
Unlike ordinary cross-origin Fetch requests, WebSocket connections are not protected by the same browser CORS enforcement model. The server must explicitly decide which origins are allowed.
Authorization Per Message
Authentication identifies the connected user. Authorization determines which actions that user may perform.
A user who is authorized to subscribe to a room may not be authorized to delete messages, change room settings, or impersonate another participant.
Each state-changing WebSocket message should pass the same business authorization checks that would apply to an equivalent REST API operation.
For broader security patterns, see API Security Patterns.
Preventing Resource Exhaustion
WebSocket servers are vulnerable to resource exhaustion when clients establish too many connections, send oversized messages, or produce data faster than the server can process it.
Useful protections include connection quotas, message-size limits, per-user rate limits, idle timeouts, and bounded outbound queues.
Rate limiting should apply to both connection attempts and message traffic. A single established WebSocket can carry thousands of messages without generating additional HTTP requests, so HTTP request-rate limits alone are insufficient.
Production Design Example
Consider a chat platform supporting 100,000 concurrently connected users. The application runs across multiple regions and uses a fleet of WebSocket gateway instances behind regional load balancers.
The main challenge is not sending one message to one socket. It is routing messages between users connected to different servers while maintaining durable message history and controlling resource consumption.
Chat System Architecture
A practical architecture separates persistent connections from durable message storage.
- WebSocket gateways: Maintain client connections, authenticate users, and validate messages.
- Message service: Applies business rules, assigns message IDs, and persists chat messages.
- Database: Stores durable conversation history and message metadata.
- Message broker: Distributes new-message notifications to gateway instances.
- Presence service: Tracks approximate online status and connection locations.
- Load balancer: Routes new connections and supports connection draining during deployments.
When a user sends a chat message, the gateway forwards it to the message service. After the message is durably accepted, the system publishes a notification that can be delivered to other connected participants.
The message broker distributes the notification to gateway instances responsible for those participants. Each gateway sends the message through its local WebSocket connections.
For a complete discussion of chat architecture, see Designing a Chat Application.
Connection Distribution and Scaling
With 100,000 concurrent users and 20 gateway instances, an even distribution would produce approximately 5,000 connections per instance.
However, connection counts are not the only capacity metric. Message rate, average payload size, fan-out, CPU usage, and outbound network bandwidth also matter.
For example, a gateway handling 5,000 mostly idle connections may consume fewer resources than one handling 1,000 connections that each receive hundreds of messages per second.
Scaling decisions should consider active connections, event-loop latency, outbound queue depth, memory, and network throughput.
Existing WebSocket connections generally remain attached to the gateway that accepted them. A load balancer distributes new connections but does not normally migrate active WebSocket sessions between application instances.
During deployments, gateways should stop accepting new connections, notify clients when appropriate, and allow a controlled reconnection period before termination.
Message Delivery and Recovery
WebSocket guarantees ordered delivery of bytes within an active TCP connection, but it does not guarantee that an application message has been durably processed or delivered to an offline recipient.
Suppose a gateway sends a chat message and crashes before receiving an application-level acknowledgment. The sender may not know whether the recipient processed the message.
Reliable applications use durable message IDs, acknowledgments, replay cursors, and idempotent processing to recover from such uncertainty.
After reconnecting, a client can request messages newer than its last confirmed message ID through REST or WebSocket.
Message ordering across multiple producers, partitions, or regions requires additional design. WebSocket itself does not impose a global order across independently generated events.
Ready-to-Use Example
The following example implements a small multi-client chat service using FastAPI and Redis Pub/Sub. Multiple FastAPI instances can share messages through Redis while each instance manages its own connected clients.
This example demonstrates distributed fan-out, connection management, message validation, and a browser client. Redis Pub/Sub is not durable, so offline users cannot recover missed messages without an additional message store.
Dependencies and Setup
Install the required packages:
pip install fastapi "uvicorn[standard]" redis
Start Redis locally:
docker run --rm -d \
--name websocket-redis \
-p 127.0.0.1:6379:6379 \
redis:7-alpine
The service uses one Redis Pub/Sub channel for chat messages. Every application instance subscribes to the channel and forwards incoming messages to its locally connected clients.
FastAPI Chat Server
Save the following code as app.py. The application manages connected clients, publishes validated messages to Redis, and broadcasts received messages through local WebSocket connections.
import asyncio
import json
import os
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, WebSocket
from starlette.websockets import WebSocketDisconnect
from redis.asyncio import Redis
from redis.exceptions import RedisError
REDIS_URL = os.getenv(
"REDIS_URL",
"redis://localhost:6379/0",
)
CHANNEL = "chat:messages"
MAX_MESSAGE_LENGTH = 2000
class ConnectionManager:
def __init__(self):
self.connections = set()
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.connections.add(websocket)
def disconnect(self, websocket: WebSocket):
self.connections.discard(websocket)
async def broadcast(self, payload: str):
disconnected = []
for websocket in list(self.connections):
try:
await asyncio.wait_for(
websocket.send_text(payload),
timeout=2.0,
)
except Exception:
disconnected.append(websocket)
for websocket in disconnected:
self.disconnect(websocket)
try:
await websocket.close()
except Exception:
pass
manager = ConnectionManager()
async def redis_listener(redis: Redis):
async with redis.pubsub() as pubsub:
await pubsub.subscribe(CHANNEL)
while True:
message = await pubsub.get_message(
ignore_subscribe_messages=True,
timeout=1.0,
)
if message is not None:
await manager.broadcast(
message["data"]
)
@asynccontextmanager
async def lifespan(app: FastAPI):
redis = Redis.from_url(
REDIS_URL,
decode_responses=True,
)
app.state.redis = redis
listener = asyncio.create_task(
redis_listener(redis)
)
try:
yield
finally:
listener.cancel()
try:
await listener
except asyncio.CancelledError:
pass
await redis.aclose()
app = FastAPI(lifespan=lifespan)
@app.websocket("/ws/chat")
async def chat(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
text = await websocket.receive_text()
if not text or len(text) > MAX_MESSAGE_LENGTH:
await websocket.send_json({
"type": "error",
"message": "Invalid message length",
})
continue
payload = json.dumps({
"type": "chat.message",
"message_id": uuid.uuid4().hex,
"text": text,
})
try:
await websocket.app.state.redis.publish(
CHANNEL,
payload,
)
except RedisError:
await websocket.send_json({
"type": "error",
"message": "Message service unavailable",
})
except WebSocketDisconnect:
pass
finally:
manager.disconnect(websocket)
Every FastAPI instance maintains its own set of connected sockets. Redis Pub/Sub distributes chat messages between instances, so a client connected to one process can receive messages published through another.
The example intentionally keeps the architecture small. The broadcast loop sends messages sequentially and can be delayed by slow clients. Production systems should use bounded per-client outbound queues and independent sender tasks.
The implementation also lacks authentication, durable message storage, delivery acknowledgments, and structured message validation beyond length checks. These are important additions before deploying a real chat application.
Browser Chat Client
Create a file named index.html with the following content:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Chat</title>
</head>
<body>
<h2>WebSocket Chat</h2>
<div id="messages"></div>
<input
id="message"
type="text"
placeholder="Type a message"
>
<button id="send">Send</button>
<script>
const socket = new WebSocket(
"ws://127.0.0.1:8000/ws/chat"
);
const messages = document.getElementById(
"messages"
);
const input = document.getElementById(
"message"
);
socket.addEventListener("message", (event) => {
const payload = JSON.parse(event.data);
const item = document.createElement("p");
item.textContent =
payload.text || payload.message;
messages.appendChild(item);
});
document.getElementById("send")
.addEventListener("click", () => {
if (socket.readyState !== WebSocket.OPEN) {
return;
}
socket.send(input.value);
input.value = "";
});
</script>
</body>
</html>
The client sends text messages over the WebSocket connection and displays messages received from the server.
Using textContent instead of inserting received text as HTML prevents messages from being interpreted as executable markup.
Running and Testing
Start the FastAPI application:
uvicorn app:app --host 127.0.0.1 --port 8000
Open index.html in two browser tabs. Sending a message from either tab should display it in both tabs.
To test communication across multiple application instances, start another FastAPI process on port 8001:
uvicorn app:app --host 127.0.0.1 --port 8001
Connect one browser tab to port 8000 and another to port 8001 by changing the WebSocket URL in the client. Both instances use the same Redis channel, so messages should propagate between them.
To inspect the WebSocket handshake, open the browser's developer tools, navigate to the Network panel, and select the WebSocket connection. Most browsers provide a Messages or Frames view showing traffic exchanged over the connection.
For a production deployment, replace ws:// with wss://, add authentication and origin validation, and introduce durable message storage if messages must survive disconnections.
Common Mistakes
WebSocket applications often fail under production load because connection state and message delivery are treated as if they behave like ordinary HTTP requests.
Not Implementing Client Reconnection
Browsers do not automatically reconnect WebSocket connections. A network interruption, server restart, or deployment can leave clients permanently disconnected.
Implement bounded exponential backoff with jitter and restore subscriptions or message cursors after reconnecting.
Keeping All Connection State in One Process
A process-local connection manager cannot deliver messages to clients connected to other application instances.
Use a shared messaging mechanism for cross-instance fan-out and durable storage when events must survive process failures.
Ignoring Slow Consumers
If the server produces messages faster than a client can receive them, outbound buffers may grow without bound.
Use bounded queues, per-client sender tasks, and explicit policies for disconnecting or resynchronizing slow consumers.
Trusting Incoming Messages
WebSocket messages are untrusted input. A valid connection does not guarantee that every message is correctly formatted or authorized.
Validate message schemas, enforce size limits, and perform authorization checks for each operation.
Assuming Successful Send Means Delivery
A successful WebSocket send operation does not prove that the recipient processed the message or that it was durably stored.
Use application-level acknowledgments, message IDs, and replay mechanisms when delivery guarantees matter.
Production Checklist
Before deploying WebSocket services, validate connection behavior, security boundaries, and resource consumption under realistic workloads.
- Transport: Use
wss://for production connections. - Authentication: Authenticate connections and validate browser origins.
- Authorization: Check permissions for each sensitive message or operation.
- Connection management: Implement heartbeats, idle timeouts, and graceful shutdown.
- Reconnection: Use exponential backoff with jitter and restore application state.
- Scaling: Support cross-instance message distribution and monitor connection counts.
- Backpressure: Bound outbound queues and handle slow consumers explicitly.
- Reliability: Use durable message storage and acknowledgments when delivery guarantees are required.
- Observability: Track active connections, message rates, errors, reconnects, and event-loop latency.
Frequently Asked Questions
WebSocket is widely supported, but its behavior differs from traditional HTTP APIs in ways that affect infrastructure, authentication, and reliability.
Is WebSocket Faster Than HTTP?
WebSocket can reduce overhead for frequent bidirectional messages because it avoids creating a new HTTP request for each exchange.
However, ordinary HTTP is often simpler and sufficiently efficient for infrequent operations. WebSocket is not inherently faster for every workload.
Does WebSocket Use TCP or UDP?
Traditional WebSocket connections use TCP, optionally protected by TLS. WebSocket over HTTP/2 also uses TCP-based transport.
WebSocket over HTTP/3 can use QUIC-based transport where supported, but deployment support and protocol negotiation differ from traditional WebSocket implementations.
Can WebSocket Work Behind a Load Balancer?
Yes. Modern load balancers commonly support WebSocket connections, but connection timeouts, routing behavior, and graceful draining must be configured appropriately.
Active WebSocket connections normally remain attached to the backend instance that accepted them until they close.
Can WebSocket Run on AWS Lambda?
AWS API Gateway provides managed WebSocket APIs that can invoke Lambda functions for connection lifecycle events and application messages.
However, a Lambda function does not normally maintain a persistent client socket itself. API Gateway manages the connections and provides APIs for sending messages back to connected clients.
Can WebSocket Messages Be Lost?
Yes. Network failures, server crashes, and application errors can interrupt delivery. TCP provides reliable ordered byte transport while a connection is functioning, but it does not guarantee application-level processing or durable message storage.
Applications requiring reliable delivery should use message IDs, acknowledgments, persistent storage, and replay after reconnection.
Conclusion
WebSocket enables persistent, bidirectional communication between clients and servers, making it a practical choice for interactive applications that exchange frequent real-time messages.
Its main advantage is efficient full-duplex communication over a long-lived connection. The trade-off is operational complexity: authentication, connection lifecycle, message routing, backpressure, reconnection, and delivery guarantees must be handled explicitly.
Key Takeaway: WebSocket is a transport protocol for real-time bidirectional messaging, not a complete messaging system. Use it when both sides need frequent communication, and combine it with durable storage, acknowledgments, and distributed messaging when reliability and scale matter.
More Articles to Read
These related articles cover communication protocols, distributed messaging, and real-time application architecture.
Comments (0)