
One minute chat - WebSocket from scratch - Part 0
I want to understand how a WebSocket works under the hood. The best way to achieve that is by building one myself, following RFC 6455. This series will focus on two main components: the server and the client. First, I’ll build the general structure using high-level frameworks. Then, I’ll implement the server from scratch — receiving requests, parsing them, performing the handshake, and managing communication. I’ll repeat this process in different languages and technologies to deepen my understanding.
Project Goal
The project is very simple: a chat that can keep messages for one minute before removing them, similar to a ephemeral
chat. Many clients can connect to the server and send messages to each other. The flow is as follows:
- Server listens for requests
- Client request for existent messages
- Server replies the stored messages
- Client rendering messages
- Client request WebSocket handshake
- Server accept handshake
- Both upgrade connection
- Client listens for messages and send them to each client
- After one minute the message is removed from both client and server
High-level stack
For this general implementation, I used two high-level frameworks: for the client, Vue
with JavaScript
, and for the server, FastAPI
with Python
. The current client implementation will be reused later when I build the server from scratch. Likewise, this server will serve as a base when I implement a low-level client.
The code for all implementations is available on GitHub. The code described in each post will include only the architectural part; the styles, HTML, and decorative elements will be omitted.
Setup
For the server I need to install fastapi[standard]
according to FastAPI documentation for using the server in development mode.
pip install fastapi[standard]
For Vue
, I think using Vite
is the straightforward way for initialize it.
pnpm create vite@latest
Implementation
Server
The general structure is built using FastAPI
. It is composed of the Message
class, which stores the text and the timestamp for tracking it. CORS
configuration is required because FastAPI
blocks CORS
by default. The lifespan
handles the entire lifecycle of the server. The clients are saved in the clients
list, which stores each WebSocket
instance. remove_old_messages
is a background task that removes messages that exceed the MESSAGE_DISAPPEAR_THRESHOLD
. The server is listens on port 8000
.
import asyncio
import logging
import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, WebSocketException
from fastapi.middleware.cors import CORSMiddleware
LOGGER_FORMAT = "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
logging.basicConfig(format=LOGGER_FORMAT)
logger = logging.getLogger(__name__)
logger.name
logger.setLevel(logging.INFO)
@asynccontextmanager
async def lifespan(_: FastAPI):
# Startup: Create the background task for message cleanup
cleanup_task = asyncio.create_task(remove_old_messages())
yield
# Shutdown: Cancel the background task
cleanup_task.cancel()
try:
await cleanup_task
except asyncio.CancelledError:
pass
app = FastAPI(lifespan=lifespan)
clients: list[WebSocket] = []
class Message:
"""Message structure"""
message: str
timestamp: float
def __init__(self, message: str, timestamp: float):
self.message = message
self.timestamp = timestamp
messages: list[Message] = []
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
MESSAGE_DISAPPEAR_THRESHOLD = 60 # Seconds
async def remove_old_messages():
"""Remove old messages in background"""
while True:
now = time.time()
global messages
bef_messages = len(messages)
messages = [msg for msg in messages if now - msg.timestamp < MESSAGE_DISAPPEAR_THRESHOLD]
aft_messages = len(messages)
logger.info("Removed %i messages", bef_messages - aft_messages)
logger.info("Total messages: %i", aft_messages)
await asyncio.sleep(5)
@app.get("/messages")
async def get_messages():
"""Return all messages"""
return {"messages": [{"message": msg.message, "timestamp": msg.timestamp} for msg in messages]}
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""Handle the WebSocket endpoint and contract"""
await websocket.accept()
logger.info("Connected to websocket %s", websocket)
clients.append(websocket)
try:
while True:
data = await websocket.receive_text()
logger.info("Received message: %s", data)
messages.append(Message(data, time.time()))
for client in clients:
await client.send_text(data)
except WebSocketException:
msg = "Error sendi message"
logger.exception(msg)
except WebSocketDisconnect:
clients.remove(websocket)
Client
In Vue
, I built a Chat
component that handles the handshake with the server and manages the messages. It has two main sections: the WebSocket
handler which connect to server in port 8080
, and the messages’ container, which requests the existing messages from the server. The client is responsible for rendering the messages and sending/receiving messages from the server.
<template>
<div class="chat-container">
<div class="messages">
<div v-for="(msg, index) in messages" :key="index" class="message">
{{ msg }}
</div>
</div>
<input v-model="newMessage" @keyup.enter="sendMessage" placeholder="Type a message..." autofocus />
</div>
</template>
<script>
export default {
data() {
return {
ws: null,
messages: [],
newMessage: ""
};
},
methods: {
sendMessage() {
if (this.newMessage.trim()) {
this.ws.send(this.newMessage);
this.newMessage = "";
}
},
async initializeMessages () {
try {
const response = await fetch("http://127.0.0.1:8000/messages", {
method: "GET",
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(response.error);
}
const data = await response.json();
// Remove messages after timeout returned from server
data.messages.forEach(msg => {
const timestamp = msg.timestamp;
// 1000 = Tranform to milliseconds and 60000 = 60 seconds
const timeout = 60000 - (Number.parseFloat(Date.now()) - timestamp * 1000);
console.log(timeout);
if (timeout > 0) {
this.messages.push(msg.message);
setTimeout(() => {
const index = this.messages.indexOf(msg.message);
if (index > -1) {
this.messages.splice(index, 1);
}
}, timeout);
}
});
} catch (error) {
console.error(error);
}
}
},
mounted() {
this.ws = new WebSocket("ws://localhost:8000/ws");
this.ws.onerror = (error) => {
console.error("WebSocket error:", error);
};
this.ws.onmessage = (event) => {
this.messages.push(event.data);
setTimeout(() => {
this.messages.shift();
}, 60000); // 60 seconds
}
this.initializeMessages();
},
beforeUnmount() {
this.ws.close();
}
};
</script>
Let’s Test It
Using the chat is very simple: just connect multiple browser tabs to the Vue client, and they’ll be synced automatically. First, I start the server, then the client.
fastapi dev
# Out:
#
# INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
FastAPI
will listen on port 8000
. To run the client (I am using pnpm instead of npm
):
pnpm run dev
# Out:
#
# VITE v6.2.4 ready in 937 ms
#
# ➜ Local: http://localhost:5173/
# ➜ Network: use --host to expose
# ➜ Vue DevTools: Open http://localhost:5173/__devtools__/ as a separate window
# ➜ Vue DevTools: Press Alt(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools
# ➜ press h + enter to show help
Visiting localhost:5173
, the chat initialized:
If I open two windows and type something into the chat, both will receive the messages.
Nice! Now I can play around in both chats — messages disappear automatically, and FastAPI
logs show the message flow when they’re received.
2025-08-07 15:49:37,990 - INFO - main - Received message: Hello
2025-08-07 15:49:41,361 - INFO - main - Received message: Hi!
2025-08-07 15:49:41,544 - INFO - main - Removed 0 messages
2025-08-07 15:49:41,545 - INFO - main - Total messages: 2
2025-08-07 15:49:46,546 - INFO - main - Removed 0 messages
2025-08-07 15:49:46,546 - INFO - main - Total messages: 2
2025-08-07 15:49:47,424 - INFO - main - Received message: Awesome!
Then when the messages are removed:
2025-08-07 15:50:41,553 - INFO - main - Removed 2 messages
2025-08-07 15:50:41,553 - INFO - main - Total messages: 1
2025-08-07 15:50:46,554 - INFO - main - Removed 0 messages
2025-08-07 15:50:46,554 - INFO - main - Total messages: 1
2025-08-07 15:50:51,554 - INFO - main - Removed 1 messages
2025-08-07 15:50:51,554 - INFO - main - Total messages: 0
Summary
I implemented a high-level version of a WebSocket
using FastAPI
and Vue
. In the next post, I will implement it using Go
and the existing client built in Vue
for testing. The goal is to implement a low-level basic WebSocket handler to truly understand how WebSockets work under RFC 6455.