Building a Blazing Fast Standalone WebSocket Server with `websockets` and ASGI
Lukas Schneider
DevOps Engineer · Leapcell

Introduction to Real-time Communication
In today's interconnected digital landscape, real-time communication is no longer a luxury but a fundamental expectation. From collaborative document editing and live chat applications to financial trading platforms and IoT device monitoring, the ability to exchange information instantly and continuously is paramount. Traditional HTTP request-response cycles, while excellent for many use cases, often fall short when persistent, low-latency, bidirectional communication is required. This is where WebSockets shine. They provide a full-duplex communication channel over a single TCP connection, drastically reducing overhead compared to repeated HTTP polling. While many Python web frameworks offer WebSocket integration, there are scenarios where a standalone, high-performance WebSocket server, decoupled from a full-fledged web framework, is the optimal solution. This article delves into how to build such a server using Python's powerful websockets
library and the Asynchronous Server Gateway Interface (ASGI) specification, unlocking efficient real-time capabilities.
Understanding the Core Components
Before diving into the implementation details, let's clarify the key technologies that underpin our high-performance WebSocket server.
WebSockets: As mentioned, WebSockets enable full-duplex communication over a single TCP connection. This means both the client and server can send and receive messages simultaneously without needing to establish new connections for each exchange. This persistent connection significantly reduces latency and overhead, making them ideal for real-time interactions.
ASGI (Asynchronous Server Gateway Interface): ASGI is a specification for Python asynchronous web servers, frameworks, and applications. It defines a standard interface for communication between asynchronous web servers (like Uvicorn or Hypercorn) and asynchronous Python web applications. ASGI applications are essentially asynchronous callables that receive a scope dictionary (containing request details) and send/receive events via send and receive functions. This standardization allows for interoperability between different ASGI servers and frameworks, promoting a robust and flexible ecosystem.
websockets
Library: This is a fantastic Python library that provides a clean and powerful API for building WebSocket servers and clients. It handles the low-level WebSocket protocol details, including handshakes, framing, and error handling, allowing developers to focus on the application logic. Its asynchronous nature aligns perfectly with ASGI and modern Python's asyncio
paradigm.
The principle behind using these together is to have an ASGI server act as the entry point, dispatching WebSocket requests to our websockets
application. The websockets
library itself can also act as a standalone server, but integrating it with an ASGI server like Uvicorn allows for greater flexibility, especially when needing to handle other ASGI-compatible protocols or integrate with ASGI middleware.
Building a High-Performance WebSocket Server
Our goal is to create a server that can handle numerous concurrent WebSocket connections efficiently. This involves asynchronous programming and careful resource management.
Simple Echo Server
Let's start with a basic echo server. When a client sends a message, the server simply sends it back. This demonstrates the core send and receive functionality.
# echo_server.py import asyncio import websockets async def echo(websocket, path): """ Asynchronous handler for WebSocket connections. Echos back any message received from the client. """ print(f"Client connected: {websocket.remote_address}") try: async for message in websocket: print(f"Received message from {websocket.remote_address}: {message}") await websocket.send(f"Echo: {message}") except websockets.exceptions.ConnectionClosedOK: print(f"Client {websocket.remote_address} disconnected gracefully") except websockets.exceptions.ConnectionClosedError as e: print(f"Client {websocket.remote_address} disconnected with error: {e}") finally: print(f"Client disconnected: {websocket.remote_address}") async def main(): """ Starts the WebSocket server. """ # Start the WebSocket server on localhost, port 8765 async with websockets.serve(echo, "localhost", 8765): await asyncio.Future() # Run forever if __name__ == "__main__": print("Starting WebSocket echo server on ws://localhost:8765") asyncio.run(main())
To run this, simply save it as echo_server.py
and execute python echo_server.py
. You can then test it using a simple JavaScript client in your browser's console:
const ws = new WebSocket("ws://localhost:8765"); ws.onopen = () => console.log("Connected"); ws.onmessage = (event) => console.log("Received:", event.data); ws.send("Hello, WebSocket!");
Integrating with ASGI for Enhanced Flexibility
While the websockets.serve
function is excellent for standalone WebSocket applications, integrating with an ASGI server like Uvicorn offers benefits such as:
- Running other ASGI applications (e.g., REST APIs) alongside WebSockets on the same server.
- Leveraging ASGI middleware.
- Better process management and scaling capabilities provided by production-ready ASGI servers.
Here's how to wrap our websockets
handler in an ASGI application. The websockets
library provides websockets.ASGIHandler
to simplify this.
# asgi_websocket_server.py import asyncio import websockets from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError from websockets.sync.server import serve import uvicorn async def websocket_application(scope, receive, send): """ ASGI-compatible WebSocket application for our echo server. """ if scope['type'] == 'websocket': async def handler(websocket): print(f"ASGI Client connected: {websocket.remote_address}") try: async for message in websocket: print(f"ASGI Received message from {websocket.remote_address}: {message}") await websocket.send(f"ASGI Echo: {message}") except ConnectionClosedOK: print(f"ASGI Client {websocket.remote_address} disconnected gracefully") except ConnectionClosedError as e: print(f"ASGI Client {websocket.remote_address} disconnected with error: {e}") finally: print(f"ASGI Client disconnected: {websocket.remote_address}") # Use websockets.server.serve protocol for ASGI # This creates an AsyncWebSocketServerProtocol instance for the connection await websockets.server.serve_websocket(handler, scope, receive, send) else: # Handle other types of requests if necessary (e.g., HTTP for health checks) # For a pure WebSocket server, this might just be an error. response_start = {'type': 'http.response.start', 'status': 404, 'headers': []} response_body = {'type': 'http.response.body', 'body': b'Not Found'} await send(response_start) await send(response_body) # To run with Uvicorn: # uvicorn asgi_websocket_server:websocket_application --port 8000 --ws websockets
To run this ASGI-compatible server:
- Install Uvicorn:
pip install uvicorn websockets
- Run the command:
uvicorn asgi_websocket_server:websocket_application --port 8000 --ws websockets
The --ws websockets
flag tells Uvicorn to use websockets
for handling WebSocket connections, ensuring compatibility with our application. Now, your JavaScript client should point to ws://localhost:8000
.
Real-world Example: A Simple Chat Room
Let's expand the echo server into a basic chat room to demonstrate handling multiple clients and broadcasting messages.
# chat_server.py import asyncio import websockets from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError import json CONNECTED_CLIENTS = set() # Store active WebSocket connections async def register(websocket): """Registers a new client connection.""" CONNECTED_CLIENTS.add(websocket) print(f"Client connected: {websocket.remote_address}. Total clients: {len(CONNECTED_CLIENTS)}") async def unregister(websocket): """Unregisters a client connection.""" CONNECTED_CLIENTS.remove(websocket) print(f"Client disconnected: {websocket.remote_address}. Total clients: {len(CONNECTED_CLIENTS)}") async def broadcast_message(message): """Sends a message to all connected clients.""" if CONNECTED_CLIENTS: # Ensure there are clients to send to await asyncio.wait([client.send(message) for client in CONNECTED_CLIENTS]) async def chat_handler(websocket, path): """ Handles individual client connections in the chat room. """ await register(websocket) try: user_name = None async for message_str in websocket: try: message_data = json.loads(message_str) message_type = message_data.get("type") if message_type == "join": user_name = message_data.get("name", "Anonymous") join_msg = json.dumps({"type": "status", "message": f"{user_name} joined the chat."}) await broadcast_message(join_msg) elif message_type == "chat" and user_name: chat_msg = message_data.get("message", "") full_msg = json.dumps({"type": "chat", "sender": user_name, "message": chat_msg}) await broadcast_message(full_msg) else: await websocket.send(json.dumps({"type": "error", "message": "Invalid message format or not joined."})) except json.JSONDecodeError: print(f"Received invalid JSON from {websocket.remote_address}: {message_str}") await websocket.send(json.dumps({"type": "error", "message": "Invalid JSON format."})) except Exception as e: print(f"Error handling message from {websocket.remote_address}: {e}") await websocket.send(json.dumps({"type": "error", "message": f"Server error: {e}"})) except ConnectionClosedOK: print(f"Client {websocket.remote_address} disconnected gracefully") except ConnectionClosedError as e: print(f"Client {websocket.remote_address} disconnected with error: {e}") finally: if user_name: leave_msg = json.dumps({"type": "status", "message": f"{user_name} left the chat."}) await broadcast_message(leave_msg) await unregister(websocket) async def main_chat_server(): """Starts the chat WebSocket server.""" async with websockets.serve(chat_handler, "localhost", 8766): await asyncio.Future() # Run forever if __name__ == "__main__": print("Starting WebSocket chat server on ws://localhost:8766") asyncio.run(main_chat_server())
This chat server allows users to join with a name and send messages that are broadcast to everyone. It uses a global set CONNECTED_CLIENTS
to keep track of active connections and asyncio.wait
for efficient broadcasting.
Application Scenarios
A standalone, high-performance WebSocket server built with websockets
and ASGI is ideal for:
- Real-time Dashboards: Displaying live data updates (stock prices, sensor readings, analytics).
- Multiplayer Games: Low-latency communication for game state synchronization.
- Live Chat and Messaging: Building custom chat applications without framework overhead.
- IoT Device Communication: Receiving real-time data streams from connected devices.
- Notification Systems: Pushing instant notifications to clients.
Conclusion: Empowering Real-time Python Applications
By combining the robustness of the websockets
library with the flexibility and standardization of ASGI, Python developers can craft powerful, standalone WebSocket servers capable of handling demanding real-time communication needs. This approach provides fine-grained control, excellent performance, and a clear path for scaling, making it an invaluable pattern for modern, interactive web services. Leveraging these tools enables Python to stand as a strong contender in the real-time application domain.