- One request returns one response
- The connection closes after the exchange
Websocket Protocol
Socket Programming with a FastAPI real-time demo chat app
Amirhossein Khalili • Morteza Khanbabaie • Alireza Khosravi
WebSocket History Timeline
The communication model evolved from simple request/response to persistent real-time channels.
HTTP
A short-lived request/response cycle with no native live push.
- Every update needs a new HTTP cycle
- The server cannot push live data by itself
Polling
The browser repeatedly asks the server for new data.
- The client checks on a fixed timer
- The server answers even when nothing changed
- Many empty requests waste network work
- Latency depends on the timer interval
Long Polling
The server holds the request until an update is available.
- The server keeps the request open
- The client reconnects after each response
- Open requests consume server state
- Reconnect and timeout handling is required
WebSocket
An HTTP Upgrade creates one persistent Full-duplex channel.
- HTTP Upgrade switches the protocol
- Frames move both ways on one socket
- Each client keeps a live connection
- Scaling needs connection-aware design
WebSocket in the OSI Model
Application semantics at Layer 7, reliable byte delivery through TCP/IP.
HTTP Versions in WebSocket
Different HTTP versions start WebSocket connections differently.
- The browser sends a normal GET request with
Upgrade: websocket. - The server replies
101 Switching Protocolsif it accepts. - After that, the same TCP connection carries WebSocket frames both ways.
- This is the classic and easiest model to understand.
- HTTP/2 does not use the old connection-level Upgrade flow.
- It uses Extended CONNECT, so WebSocket traffic lives inside one HTTP/2 stream.
- Other HTTP/2 streams can share the same underlying connection.
- Support depends more on servers, clients, and proxies.
- HTTP/3 runs on QUIC over UDP instead of TCP.
- WebSocket uses a CONNECT-based mapping similar in idea to HTTP/2.
- QUIC streams reduce transport-level head-of-line blocking.
- It is modern, but the demo is easier to explain with HTTP/1.1.
Project Architecture
Browsers connect to one WebSocket endpoint; ConnectionManager groups sockets by room.
Backend Codes
WebSocket router setup in FastAPI.
import json
from datetime import datetime, timezone
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from app.websocket.manager import manager
router = APIRouter()
Message Payload Shape
Every server message uses one small JSON structure.
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def event(event_type, room_id, client_id, content):
return {
"type": event_type,
"room_id": room_id,
"client_id": client_id,
"content": content,
"timestamp": now_iso(),
}
WebSocket Endpoint
The route parameters select the room and the user identity.
@router.websocket("/ws/{room_id}/{client_id}")
async def websocket_endpoint(
websocket: WebSocket,
room_id: str,
client_id: str,
):
room_id = clean_value(room_id, "general")
client_id = clean_value(client_id, "anonymous")
await manager.connect(websocket, room_id, client_id)
Join and Presence Events
After connection, the server informs the room and refreshes online users.
await manager.broadcast_json(
event("system", room_id, "system", f"{client_id} joined {room_id}."),
room_id,
)
await manager.broadcast_json({
"type": "presence",
"users": manager.room_users(room_id),
"timestamp": now_iso(),
}, room_id)
Receive Loop and Broadcast
The socket stays open and every valid message is broadcast to the room.
while True:
raw_data = await websocket.receive_text()
try:
data = json.loads(raw_data)
message = str(data.get("content", "")).strip()
except json.JSONDecodeError:
message = raw_data.strip()
if message:
await manager.broadcast_json(event("message", room_id, client_id, message), room_id)
ConnectionManager
The manager groups sockets by room and removes dead connections.
class ConnectionManager:
def __init__(self):
self.active_connections = {}
async def connect(self, websocket, room_id, client_id):
await websocket.accept()
self.active_connections.setdefault(room_id, [])
self.active_connections[room_id].append(ClientConnection(websocket, client_id))
async def broadcast_json(self, payload, room_id):
for connection in list(self.active_connections.get(room_id, [])):
await connection.websocket.send_json(payload)
Frontend Codes
browser state and DOM references.
const usernameInput = document.querySelector('#username');
const roomInput = document.querySelector('#room');
const connectBtn = document.querySelector('#connectBtn');
const messageForm = document.querySelector('#messageForm');
const messages = document.querySelector('#messages');
const usersList = document.querySelector('#usersList');
let socket = null;
let currentUser = '';
let currentRoom = '';
Building the WebSocket URL
The browser automatically switches between ws and wss.
function connect() {
currentUser = safeName(usernameInput.value, 'anonymous');
currentRoom = safeName(roomInput.value, 'general');
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const room = encodeURIComponent(currentRoom);
const user = encodeURIComponent(currentUser);
const wsUrl = `${wsProtocol}://${window.location.host}/ws/${room}/${user}`;
socket = new WebSocket(wsUrl);
}
Connection Open Handler
When the socket opens, the UI moves into connected mode.
socket.addEventListener('open', () => {
setConnectedState(true);
roomTitle.textContent = `Room: ${currentRoom}`;
roomSubtitle.textContent = `You are chatting as ${currentUser}.`;
messageInput.focus();
});
Handling Incoming Messages
Presence updates and chat messages are separated by type.
socket.addEventListener('message', (event) => {
const payload = JSON.parse(event.data);
if (payload.type === 'presence') {
updateUsers(payload.users || []);
return;
}
addMessage(payload);
});
Rendering a Chat Message
The browser creates message elements without reloading the page.
function addMessage(payload) {
clearEmptyState();
const card = document.createElement('article');
card.className = 'message';
if (payload.client_id === currentUser) card.classList.add('mine');
content.textContent = payload.content || '';
card.append(meta, content);
messages.appendChild(card);
messages.scrollTop = messages.scrollHeight;
}
Sending Messages
The form sends JSON only when the socket is open.
messageForm.addEventListener('submit', (event) => {
event.preventDefault();
const content = messageInput.value.trim();
if (!content || !socket || socket.readyState !== WebSocket.OPEN) return;
socket.send(JSON.stringify({ content }));
messageInput.value = '';
messageInput.focus();
});
Keynotes and Takeaways
The essential points to remember from the project.
- Simple
- Stateless
- Not ideal for live push
- Easy
- Wasteful
- Interval-based latency
- Persistent
- Full-duplex
- Low latency
- Connection state
- Disconnect handling
- Scaling strategy