0%

Real-Time Communication: WebSockets with Python

Build live chat applications, real-time dashboards, and instant notifications. Two-way communication between browser and server without HTTP polling.

HTTP is one-way. The client asks, the server answers. The client waits. If the server has news, it cannot send it. The client must ask again. And again. This is polling. It is inefficient. It is slow. WebSockets are different. Once connected, both sides can send messages. The server can push data instantly. The client can send messages anytime. The connection stays open. This is real-time communication. This tutorial teaches you to build WebSocket applications with Python. You will use the websockets library (for servers) and JavaScript WebSocket API (for clients). You will build a live chat application, a real-time counter, and a simple game. By the end, you will understand bidirectional, persistent connections.

🕯️ Magic Note

WebSocket is a protocol standardized in 2011 (RFC 6455). It provides full-duplex communication over a single TCP connection. Unlike HTTP, which requires a new connection for each request, WebSocket stays open, allowing instant data transfer in both directions.

WebSocket vs HTTP: Understanding the Difference
Why WebSockets are needed and when to use them.
FeatureHTTPWebSocket
DirectionClient → Server (request-response)Bidirectional (client ↔ server)
ConnectionShort-lived (closed after response)Persistent (stays open)
Server PushNot possible without pollingNative support
OverheadHeaders per request (hundreds of bytes)Minimal after handshake
LatencyHigher (new connection each time)Lower (messages sent immediately)
Use CaseREST APIs, web pages, file downloadsChat, games, live dashboards, notifications

🕯️ Magic Note

HTTP polling: client asks every second “Any news?” Most answers are “No.” WebSocket: client connects once, server says “News!” when it arrives. WebSocket saves bandwidth and reduces latency.

Installing the websockets Library
Install the server-side WebSocket library.

Bash

pip install websockets

Your First WebSocket Server
A simple echo server that responds to messages.

Python (echo_server.py)

import asyncio

import websockets

async def echo(websocket, path):

print(f”Client connected from {websocket.remote_address}”)

try:

async for message in websocket:

print(f”Received: {message}”)

# Echo the message back to the client

await websocket.send(f”Echo: {message}”)

except websockets.exceptions.ConnectionClosed:

print(f”Client {websocket.remote_address} disconnected”)

async def main():

async with websockets.serve(echo, “localhost”, 8765):

print(“WebSocket server started on ws://localhost:8765”)

await asyncio.Future() # Run forever

if __name__ == “__main__”:

asyncio.run(main())

Python (echo_client.py)

import asyncio

import websockets

async def test_client():

uri = “ws://localhost:8765”

async with websockets.connect(uri) as websocket:

# Send a message

await websocket.send(“Hello, WebSocket!”)

print(“Sent: Hello, WebSocket!”)

# Receive response

response = await websocket.recv()

print(f”Received: {response}”)

asyncio.run(test_client())

🕯️ Magic Note

The WebSocket server uses `async for message in websocket` to receive messages. This pattern is clean and handles disconnections gracefully. The `ConnectionClosed` exception is raised when the client disconnects.

Broadcast Server: Sending to All Clients
A chat server that broadcasts messages to all connected clients.

Python (chat_server.py)

import asyncio

import websockets

from datetime import datetime

connected_clients = set()

async def broadcast(message):

“””Send a message to all connected clients.”””

if connected_clients:

await asyncio.wait([client.send(message) for client in connected_clients])

async def chat_handler(websocket, path):

# Add client to the set

connected_clients.add(websocket)

print(f”Client connected. Total: {len(connected_clients)}”)

# Send welcome message

await websocket.send(“Welcome to the chat!”)

await broadcast(f”[System] New user joined ({len(connected_clients)} online)”)

try:

async for message in websocket:

timestamp = datetime.now().strftime(“%H:%M:%S”)

formatted_message = f”[{timestamp}] {message}”

print(formatted_message)

await broadcast(formatted_message)

except websockets.exceptions.ConnectionClosed:

print(“Client disconnected”)

finally:

connected_clients.remove(websocket)

await broadcast(f”[System] User left ({len(connected_clients)} online)”)

async def main():

async with websockets.serve(chat_handler, “localhost”, 8765):

print(“Chat server started on ws://localhost:8765”)

await asyncio.Future()

if __name__ == “__main__”:

asyncio.run(main())

HTML + JavaScript (chat_client.html)

<!DOCTYPE html>

<html>

<head>

<title>WebSocket Chat</title>

</head>

<body>

<div id=”messages” style=”height: 300px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px;”></div>

<input type=”text” id=”messageInput” placeholder=”Type a message…” style=”width: 80%;”>

<button id=”sendButton”>Send</button>

<script>

const messagesDiv = document.getElementById(“messages”);

const input = document.getElementById(“messageInput”);

const button = document.getElementById(“sendButton”);

// Connect to WebSocket server

const ws = new WebSocket(“ws://localhost:8765”);

ws.onopen = function() {

addMessage(“[System] Connected to chat server”);

};

ws.onmessage = function(event) {

addMessage(event.data);

};

ws.onclose = function() {

addMessage(“[System] Disconnected from chat server”);

};

function addMessage(text) {

const div = document.createElement(“div”);

div.textContent = text;

messagesDiv.appendChild(div);

messagesDiv.scrollTop = messagesDiv.scrollHeight;

}

function sendMessage() {

const message = input.value.trim();

if (message) {

ws.send(message);

input.value = “”;

}

}

button.onclick = sendMessage;

input.onkeypress = function(e) {

if (e.key === “Enter”) sendMessage();

};

</script>

</body>

</html>

🕯️ Magic Note

Open the HTML file in multiple browser windows. Type messages in one window. They appear in all windows. This is a working chat application in less than 100 lines of Python and 50 lines of JavaScript.

Real-Time Counter Example
A server that pushes counter updates every second to all clients.

Python (counter_server.py)

import asyncio

import websockets

connected_clients = set()

counter = 0

async def counter_updater():

“””Update counter every second and broadcast to all clients.”””

global counter

while True:

await asyncio.sleep(1)

counter += 1

if connected_clients:

message = f”{counter}”

await asyncio.wait([client.send(message) for client in connected_clients])

async def handler(websocket, path):

connected_clients.add(websocket)

print(f”Client connected. Total: {len(connected_clients)}”)

try:

await websocket.wait_closed()

finally:

connected_clients.remove(websocket)

print(f”Client disconnected. Total: {len(connected_clients)}”)

async def main():

# Run counter updater in background

asyncio.create_task(counter_updater())

async with websockets.serve(handler, “localhost”, 8765):

print(“Counter server started on ws://localhost:8765”)

await asyncio.Future()

if __name__ == “__main__”:

asyncio.run(main())

HTML + JavaScript (counter_client.html)

<!DOCTYPE html>

<html>

<head>

<title>Real-Time Counter</title>

</head>

<body>

<h1>Real-Time Counter</h1>

<div style=”font-size: 72px; text-align: center; padding: 50px;” id=”counter”>0</div>

<script>

const counterDiv = document.getElementById(“counter”);

const ws = new WebSocket(“ws://localhost:8765”);

ws.onmessage = function(event) {

counterDiv.textContent = event.data;

};

ws.onclose = function() {

counterDiv.textContent = “Disconnected”;

};

</script>

</body>

</html>

🕯️ Magic Note

Open the HTML file in multiple browser windows. The counter updates simultaneously in all windows. This demonstrates server push—the server sends updates without the client asking for them.

Client-to-Client Messaging (Private Messages)
Extend the chat server to support private messages.

Python (private_chat_server.py)

import asyncio

import websockets

import json

clients = {} # username -> websocket

async def broadcast(message, exclude=None):

“””Send message to all connected clients.”””

for client in clients.values():

if client != exclude:

try:

await client.send(message)

except:

pass

async def chat_handler(websocket, path):

# Wait for username

username = await websocket.recv()

if username in clients:

await websocket.send(“ERROR: Username already taken”)

await websocket.close()

return

clients[username] = websocket

print(f”{username} joined. Total: {len(clients)}”)

await broadcast(json.dumps({“type”: “system”, “message”: f”{username} joined”}))

try:

async for raw_message in websocket:

data = json.loads(raw_message)

if data[“type”] == “public”:

await broadcast(json.dumps({

“type”: “public”,

“from”: username,

“message”: data[“message”]

}))

elif data[“type”] == “private”:

target = data[“to”]

if target in clients:

await clients[target].send(json.dumps({

“type”: “private”,

“from”: username,

“message”: data[“message”]

}))

await websocket.send(json.dumps({

“type”: “private_sent”,

“to”: target,

“message”: data[“message”]

}))

else:

await websocket.send(json.dumps({

“type”: “error”,

“message”: f”User {target} not found”

}))

except websockets.exceptions.ConnectionClosed:

pass

finally:

del clients[username]

await broadcast(json.dumps({“type”: “system”, “message”: f”{username} left”}))

print(f”{username} left. Total: {len(clients)}”)

async def main():

async with websockets.serve(chat_handler, “localhost”, 8765):

print(“Private chat server started on ws://localhost:8765”)

await asyncio.Future()

if __name__ == “__main__”:

asyncio.run(main())

Error Handling and Reconnection
Robust client with automatic reconnection.

JavaScript (reconnecting_client.js)

class ReconnectingWebSocket {

constructor(url, options = {}) {

this.url = url;

this.maxRetries = options.maxRetries || 5;

this.retryDelay = options.retryDelay || 3000;

this.onMessage = options.onMessage || (() => {});

this.onConnect = options.onConnect || (() => {});

this.onDisconnect = options.onDisconnect || (() => {});

this.retryCount = 0;

this.connect();

}

connect() {

this.ws = new WebSocket(this.url);

this.ws.onopen = () => {

console.log(“Connected to WebSocket server”);

this.retryCount = 0;

this.onConnect();

};

this.ws.onmessage = (event) => {

this.onMessage(event.data);

};

this.ws.onclose = () => {

console.log(“WebSocket connection closed”);

this.onDisconnect();

this.reconnect();

};

this.ws.onerror = (error) => {

console.error(“WebSocket error:”, error);

};

}

reconnect() {

if (this.retryCount >= this.maxRetries) {

console.log(“Max retries reached. Giving up.”);

return;

}

this.retryCount++;

const delay = this.retryDelay * Math.pow(2, this.retryCount – 1);

console.log(`Reconnecting in ${delay/1000} seconds… (attempt ${this.retryCount}/${this.maxRetries})`);

setTimeout(() => this.connect(), delay);

}

send(data) {

if (this.ws && this.ws.readyState === WebSocket.OPEN) {

this.ws.send(data);

} else {

console.warn(“Cannot send message: WebSocket not open”);

}

}

}

Security Considerations
Important security notes for WebSocket applications.
  • ⚠️ Validate and sanitize all messages (do not trust client input)
  • ⚠️ Implement authentication before allowing WebSocket connections
  • ⚠️ Use `wss://` (WebSocket Secure) in production (same as HTTPS)
  • ⚠️ Set appropriate timeouts to prevent hanging connections
  • ⚠️ Limit message size to prevent denial-of-service attacks
  • ⚠️ Rate-limit messages per client

Python (secure_server.py – with authentication)

import asyncio

import websockets

import jwt # pip install pyjwt

SECRET_KEY = “your-secret-key”

async def auth_handler(websocket, path):

# Expect first message to be a JWT token

token = await websocket.recv()

try:

payload = jwt.decode(token, SECRET_KEY, algorithms=[“HS256”])

username = payload[“username”]

print(f”Authenticated user: {username}”)

# Accept the connection and continue with authenticated session

await websocket.send(“Authenticated successfully”)

# Handle messages…

async for message in websocket:

# Process authenticated message

pass

except jwt.InvalidTokenError:

await websocket.send(“Authentication failed”)

await websocket.close()

When NOT to Use WebSockets
  • **Simple REST APIs:** WebSocket overhead is unnecessary
  • **File downloads:** HTTP is better optimized
  • **Rare updates:** HTTP polling may be simpler
  • **Serverless environments:** WebSocket support varies
Common WebSocket Mistakes
  • Not handling disconnections (clients may leave without notice)
  • Sending too many messages (overwhelming clients)
  • Not implementing reconnection logic on client side
  • Using WebSocket where a simple HTTP request would suffice
  • Forgetting that WebSocket messages are asynchronous
Check Your Understanding
  • What is the main difference between HTTP and WebSocket?
  • How do you send a message to all connected clients?
  • Write a WebSocket server that echoes messages back to the sender.
  • How do you handle client disconnections gracefully?
  • What is the purpose of `async for message in websocket`?
  • Why would you implement reconnection logic on the client side?

⚡ Whisper

HTTP is request-response. Ask, then wait. Ask again. WebSocket is different. Connect once. Then send. Receive. Both ways. Any time. This is real-time. This is chat, games, live dashboards, collaborative editing. The server pushes news instantly. The client responds immediately. The connection stays open. Use WebSocket when you need speed. When you need push. When polling is too slow. Learn the protocol. Build a chat server. Broadcast messages. Handle disconnections. Implement reconnection. Your applications will come alive. Users will see changes instantly. This is not the future. This is now. Open the connection. Send the message. Watch it arrive.

Related posts