Back to Blog
Python

Python WebSockets: Client-Server Send and Receive

python websockets client server send receive: Learn how to build a WebSocket client and server in Python, send and receive messages, handle connections, and manage con...

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

python websockets client server send receive requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

To send and receive messages between a client and server over WebSocket in Python, you typically use the websockets library, which is built on asyncio. The core pattern is straightforward: the server awaits 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())

The serve coroutine takes a handler function, a host, and a port. For each incoming connection, the library creates a new task that runs the handler. Inside 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 alive until it is cancelled.

A Minimal WebSocket Client

On the client side, you 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())

The connect coroutine performs the WebSocket handshake and returns a connection object. The async with block ensures the connection is closed properly when the block exits. send and recv are both coroutines: send queues the message for transmission, and recv waits for the next message from the server.

Understanding the send and receive flow

The send and recv methods are the core of WebSocket communication. They are asymmetric: send is non-blocking in the sense that it queues the message and returns immediately, while recv blocks 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 of this.

Connection lifecycle and cleanup

WebSocket connections have a defined lifecycle: opening handshake, message exchange, and closing handshake. The websockets library manages the handshake automatically, but you can hook into lifecycle events by overriding methods in a subclass of WebSocketServerProtocol or by using the connection_made and connection_lost callbacks in your handler.

In 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. You can set a ping_timeout to detect dead connections. If a client does not respond to a ping within the timeout, the connection is closed.

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, you need to 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 unexpectedly. Your client code should catch this exception and decide whether to reconnect.

A simple reconnection loop:

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 (websockets.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, you would add a maximum retry count and exponential backoff.

Performance and operational considerations

WebSocket messages are limited in size by the protocol (typically 2^63 bytes, but practical limits are much lower). The websockets library buffers outgoing messages, so if a client is slow to read, the server's memory usage can grow. To avoid this, you can use websocket.send with a timeout or implement a backpressure mechanism by checking the connection's transport buffer size.

Timeouts are also important. Set a 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 for better performance, but measure the actual impact before adding dependencies.

python websockets client server send receive: Practical Usag | RYUSLOG DEV