Upgrade to the new asyncio implementation

The new asyncio implementation is a rewrite of the original implementation of websockets.

It provides a very similar API. However, there are a few differences.

The recommended upgrade process is:

  1. Make sure that your application doesn’t use any deprecated APIs. If it doesn’t raise any warnings, you can skip this step.

  2. Check if your application depends on missing features. If it does, you should stick to the original implementation until they’re added.

  3. Update import paths. For straightforward usage of websockets, this could be the only step you need to take. Upgrading could be transparent.

  4. Review API changes and adapt your application to preserve its current functionality or take advantage of improvements in the new implementation.

In the interest of brevity, only connect() and serve() are discussed below but everything also applies to unix_connect() and unix_serve() respectively.

What will happen to the original implementation?

The original implementation is now considered legacy.

The next steps are:

  1. Deprecating it once the new implementation reaches feature parity.

  2. Maintaining it for five years per the backwards-compatibility policy.

  3. Removing it. This is expected to happen around 2030.

Deprecated APIs

Here’s the list of deprecated behaviors that the original implementation still supports and that the new implementation doesn’t reproduce.

If you’re seeing a DeprecationWarning, follow upgrade instructions from the release notes of the version in which the feature was deprecated.

  • The path argument of connection handlers — unnecessary since 10.1 and deprecated in 13.0.

  • The loop and legacy_recv arguments of connect() and serve(), which were removed — deprecated in 10.0.

  • The timeout and klass arguments of connect() and serve(), which were renamed to close_timeout and create_protocol — deprecated in 7.0 and 3.4 respectively.

  • An empty string in the origins argument of serve() — deprecated in 7.0.

  • The host, port, and secure attributes of connections — deprecated in 8.0.

Missing features

All features listed below will be provided in a future release.

If your application relies on one of them, you should stick to the original implementation until the new implementation supports it in a future release.

HTTP Basic Authentication

On the server side, serve() doesn’t provide HTTP Basic Authentication yet.

For the avoidance of doubt, on the client side, connect() performs HTTP Basic Authentication.

Following redirects

The new implementation of connect() doesn’t follow HTTP redirects yet.

Automatic reconnection

The new implementation of connect() doesn’t provide automatic reconnection yet.

In other words, the following pattern isn’t supported:

from websockets.asyncio.client import connect

async for websocket in connect(...):  # this doesn't work yet
    ...

Import paths

For context, the websockets package is structured as follows:

  • The new implementation is found in the websockets.asyncio package.

  • The original implementation was moved to the websockets.legacy package.

  • The websockets package provides aliases for convenience.

  • The websockets.client and websockets.server packages provide aliases for backwards-compatibility with earlier versions of websockets.

  • Currently, all aliases point to the original implementation. In the future, they will point to the new implementation or they will be deprecated.

To upgrade to the new asyncio implementation, change import paths as shown in the tables below.

Client APIs

Legacy asyncio implementation

New asyncio implementation

websockets.connect()
websockets.client.connect()
websockets.legacy.client.connect()

websockets.asyncio.client.connect()

websockets.unix_connect()
websockets.client.unix_connect()
websockets.legacy.client.unix_connect()

websockets.asyncio.client.unix_connect()

websockets.WebSocketClientProtocol
websockets.client.WebSocketClientProtocol
websockets.legacy.client.WebSocketClientProtocol

websockets.asyncio.client.ClientConnection

Server APIs

Legacy asyncio implementation

New asyncio implementation

websockets.serve()
websockets.server.serve()
websockets.legacy.server.serve()

websockets.asyncio.server.serve()

websockets.unix_serve()
websockets.server.unix_serve()
websockets.legacy.server.unix_serve()

websockets.asyncio.server.unix_serve()

websockets.WebSocketServer
websockets.server.WebSocketServer
websockets.legacy.server.WebSocketServer

websockets.asyncio.server.Server

websockets.WebSocketServerProtocol
websockets.server.WebSocketServerProtocol
websockets.legacy.server.WebSocketServerProtocol

websockets.asyncio.server.ServerConnection

websockets.broadcast
websockets.legacy.server.broadcast()

websockets.asyncio.server.broadcast()

websockets.BasicAuthWebSocketServerProtocol
websockets.auth.BasicAuthWebSocketServerProtocol
websockets.legacy.auth.BasicAuthWebSocketServerProtocol

not available yet

websockets.basic_auth_protocol_factory()
websockets.auth.basic_auth_protocol_factory()
websockets.legacy.auth.basic_auth_protocol_factory()

not available yet

API changes

Controlling UTF-8 decoding

The new implementation of the recv() method provides the decode argument to control UTF-8 decoding of messages. This didn’t exist in the original implementation.

If you’re calling encode() on a str object returned by recv(), using decode=False and removing encode() saves a round-trip of UTF-8 decoding and encoding for text messages.

You can also force UTF-8 decoding of binary messages with decode=True. This is rarely useful and has no performance benefits over decoding a bytes object returned by recv().

Receiving fragmented messages

The new implementation provides the recv_streaming() method for receiving a fragmented message frame by frame. There was no way to do this in the original implementation.

Depending on your use case, adopting this method may improve performance when streaming large messages. Specifically, it could reduce memory usage.

Customizing the opening handshake

On the client side, if you’re adding headers to the handshake request sent by connect() with the extra_headers argument, you must rename it to additional_headers.

On the server side, if you’re customizing how serve() processes the opening handshake with the process_request, extra_headers, or select_subprotocol, you must update your code. process_response and select_subprotocol have new signatures; process_response replaces extra_headers and provides more flexibility.

process_request

The signature of process_request changed. This is easiest to illustrate with an example:

import http

# Original implementation

def process_request(path, request_headers):
    return http.HTTPStatus.OK, [], b"OK\n"

serve(..., process_request=process_request, ...)

# New implementation

def process_request(connection, request):
    return connection.respond(http.HTTPStatus.OK, "OK\n")

serve(..., process_request=process_request, ...)

connection is always available in process_request. In the original implementation, you had to write a subclass of WebSocketServerProtocol and pass it in the create_protocol argument to make the connection object available in a process_request method. This pattern isn’t useful anymore; you can replace it with a process_request function or coroutine.

path and headers are available as attributes of the request object.

process_response

process_request replaces extra_headers and provides more flexibility. In the most basic case, you would adapt your code as follows:

# Original implementation

serve(..., extra_headers=HEADERS, ...)

# New implementation

def process_response(connection, request, response):
    response.headers.update(HEADERS)
    return response

serve(..., process_response=process_response, ...)

connection is always available in process_response, similar to process_request. In the original implementation, there was no way to make the connection object available.

In addition, the request and response objects are available, which enables a broader range of use cases (e.g., logging) and makes process_response more useful than extra_headers.

select_subprotocol

The signature of select_subprotocol changed. Here’s an example:

# Original implementation

def select_subprotocol(client_subprotocols, server_subprotocols):
    if "chat" in client_subprotocols:
        return "chat"

# New implementation

def select_subprotocol(connection, subprotocols):
    if "chat" in subprotocols
        return "chat"

serve(..., select_subprotocol=select_subprotocol, ...)

connection is always available in select_subprotocol. This brings the same benefits as in process_request. It may remove the need to subclass of WebSocketServerProtocol.

The subprotocols argument contains the list of subprotocols offered by the client. The list of subprotocols supported by the server was removed because select_subprotocols already knows which subprotocols it may select and under which conditions.

Arguments of connect() and serve()

ws_handlerhandler

The first argument of serve() is now called handler instead of ws_handler. It’s usually passed as a positional argument, making this change transparent. If you’re passing it as a keyword argument, you must update its name.

create_protocolcreate_connection

The keyword argument of serve() for customizing the creation of the connection object is now called create_connection instead of create_protocol. It must return a ServerConnection instead of a WebSocketServerProtocol.

If you were customizing connection objects, you should check the new implementation and possibly redo your customization. Keep in mind that the changes to process_request and select_subprotocol remove most use cases for create_connection.

max_queue

The max_queue argument of connect() and serve() has a new meaning but achieves a similar effect.

It is now the high-water mark of a buffer of incoming frames. It defaults to 16 frames. It used to be the size of a buffer of incoming messages that refilled as soon as a message was read. It used to default to 32 messages.

This can make a difference when messages are fragmented in several frames. In that case, you may want to increase max_queue. If you’re writing a high performance server and you know that you’re receiving fragmented messages, probably you should adopt recv_streaming() and optimize the performance of reads again. In all other cases, given how uncommon fragmentation is, you shouldn’t worry about this change.

read_limit

The read_limit argument doesn’t exist in the new implementation because it doesn’t buffer data received from the network in a StreamReader. With a better design, this buffer could be removed.

The buffer of incoming frames configured by max_queue is the only read buffer now.

write_limit

The write_limit argument of connect() and serve() defaults to 32 KiB instead of 64 KiB.

Attributes of connections

path, request_headers and response_headers

The path, request_headers and response_headers properties are replaced by request and response, which provide a headers attribute.

If your code relies on them, you can replace:

connection.path
connection.request_headers
connection.response_headers

with:

connection.request.path
connection.request.headers
connection.response.headers

open and closed

The open and closed properties are removed. Using them was discouraged.

Instead, you should call recv() or send() and handle ConnectionClosed exceptions.

If your code relies on them, you can replace:

connection.open
connection.closed

with:

from websockets.protocol import State

connection.state is State.OPEN
connection.state is State.CLOSED