Back to Blog
Python

Python WebSockets: Client-Server Send and Receive

Learn how to build a WebSocket client and server in Python with the websockets library. This guide covers sending and receiving messages, connection handling, broadcasting, and reconnection.

websocketsasynciopython networkingreal-time communicationclient-server
Diagram of a Python WebSocket server and client exchanging messages over a persistent connection.

To send and receive WebSocket messages between a client and a server in Python, the usual approach is the websockets library, built on asyncio. The core pattern is simple: the server waits for incoming connections, and each connection can send and receive messages asynchronously.

A Minimal WebSocket Server in Python

The websockets library provides a high-level API for building WebSocket servers and clients on top of asyncio. A server that echoes every message it receives can be written in a few lines:

import asyncio import websockets async def echo(websocket): async for message in websocket: await websocket.send(message) async def main(): async with websockets.serve(echo, "localhost", 8765): await asyncio.Future() # keep the server running asyncio.run(main())

websockets.serve takes a handler function, a host, and a port. For each incoming connection, the library creates a new task that runs the handler. In the handler, async for message in websocket iterates over incoming messages, and await websocket.send(message) sends the same message back to the client. The asyncio.Future() in main keeps the server running until it is cancelled.

In recent versions of websockets, the handler receives only the connection object. Older versions also passed the request path as a second argument.

A Minimal WebSocket Client

On the client side, connect to a server URI and then send and receive messages using the same send and recv methods:

import asyncio import websockets async def client(): uri = "ws://localhost:8765" async with websockets.connect(uri) as websocket: await websocket.send("Hello, server") response = await websocket.recv() print(f"Received: {response}") asyncio.run(client())

websockets.connect establishes the WebSocket handshake and returns a connection object when used as an async context manager. The async with block ensures the connection is closed properly when the block exits. send and recv are both coroutines: send hands the message to the connection for transmission, and recv waits for the next message from the server.

Understanding the send and receive flow

send and recv are the core of WebSocket communication. They are asymmetric: send is a coroutine you await, but it does not wait for a reply from the peer; recv waits until a message arrives. This means you can have one task continuously reading messages while another task sends messages, or you can alternate between sending and receiving in a request-response pattern.

For example, a client that sends a message and waits for a specific response:

async def request_response(websocket, request): await websocket.send(request) response = await websocket.recv() return response

The server can handle this pattern by reading a message, processing it, and sending back a result. The echo server above is the simplest version.

Connection lifecycle and cleanup

WebSocket connections have a defined lifecycle: opening handshake, message exchange, and closing handshake. The websockets library manages the handshake automatically. With the handler-based API, the handler is called when the connection is established. When the handler returns or raises an exception, the connection is closed. You can also explicitly close the connection with await websocket.close().

Ping and pong frames are used to keep the connection alive. The library sends pings automatically based on the ping_interval parameter, and you can set ping_timeout to detect dead connections. If a client does not respond to a ping within the timeout, the connection is closed.

For advanced lifecycle control, you can subclass the server protocol class, override methods such as connection_made and connection_lost, and configure serve to use that subclass. The exact class and configuration option depend on your websockets version.

Handling multiple clients concurrently

Because each connection runs in its own asyncio task, the server can handle many clients at once without additional threading. To broadcast a message to all connected clients, maintain a set of active connections:

connected = set() async def handler(websocket): connected.add(websocket) try: async for message in websocket: for client in connected.copy(): if client is not websocket: await client.send(message) finally: connected.remove(websocket)

The connected set is shared across tasks. When a client sends a message, the server forwards it to every other client. The copy() prevents modification of the set while iterating. Removing the client in a finally block ensures cleanup even if the handler raises an exception.

Error handling and reconnection

Network failures and remote closures are common. The websockets library raises ConnectionClosed when the connection is terminated. Client code should catch this exception and decide whether to reconnect.

A simple reconnection loop:

import asyncio import websockets from websockets.exceptions import ConnectionClosed async def client_with_reconnect(): uri = "ws://localhost:8765" while True: try: async with websockets.connect(uri) as websocket: await websocket.send("Hello") response = await websocket.recv() print(response) break # success, exit loop except (ConnectionClosed, OSError): await asyncio.sleep(1) # wait before retrying

This loop attempts to connect, send a message, and receive a response. If the connection fails or is closed, it waits one second and tries again. In a production system, add a maximum retry count and exponential backoff.

Performance and operational considerations

WebSocket frames use a 64-bit payload length field, so the protocol allows very large messages in theory. In practice, message sizes are constrained by memory and by the max_size parameter, which controls the largest incoming message the library will accept. Outgoing messages can also accumulate in memory if a client reads slowly, so choose limits appropriate for your workload.

Timeouts are important too. Set ping_interval and ping_timeout on the server to detect dead clients. On the client side, you can use asyncio.wait_for to limit how long recv waits for a message.

For high-throughput scenarios, consider using websockets with uvloop, but measure the actual impact before adding dependencies.

Python WebSockets: Send and Receive Messages Between Client and Server | RYUSLOG DEV