Skip to content

Limiting WebSocket Message Size

A WebSocket message is reassembled in memory before your handler sees it, so the server's maximum message size is a memory limit per connection. Defaults differ by library, and the same limit can cost very different amounts of memory depending on the implementation. Measured on Python 3.14 with websockets 17.1, uvicorn 0.54 and Starlette 1.7, a client sending one 200,000,000-byte message: a websockets server with its default max_size of 1 MiB closed the connection with code 1009 in 0.56 s and peaked at 28 MiB of memory; with max_size=None it accepted the message and peaked at 408 MiB. The limit applied after decompression: the same message as zeros compressed with permessage-deflate was refused with 1009 too. uvicorn's default ws_max_size is 16 MiB: a 10 MB message passed and a 20 MB one got 1009. With 20 clients each sending 15 MB to a uvicorn server limited to 1 MiB, the websockets-sansio implementation peaked at 60 MiB and wsproto at 235 MiB. This guide sets the limit deliberately and checks what it costs.

Prerequisites

1. Know the defaults you are running

Each stack has its own limit, set in a different place:

# websockets: default max_size=2**20 (1 MiB)
async with websockets.asyncio.server.serve(handler, "0.0.0.0", 8765, max_size=2**20):
    ...

# uvicorn: default ws_max_size=16777216 (16 MiB), for any ASGI app
uvicorn.run(app, ws_max_size=1 * 1024 * 1024)        # or --ws-max-size 1048576

Measured: the websockets server closed a 200,000,000-byte message with code 1009 and the reason frame with 200000000 bytes exceeds limit of 1048576 bytes, after 0.56 s. Behind uvicorn with defaults, a Starlette WebSocket endpoint accepted a 10,000,000-byte message and closed a 20,000,000-byte one with 1009. A service that moves from a plain websockets server to an ASGI framework silently goes from a 1 MiB limit to 16 MiB.

Verify: the effective limit is tested by sending a message just above it and checking for close code 1009.

Message size limits, measured A grid of 7 rows by 3 columns. Message size limits, measured setup message result websockets, default max_size (1 MiB) 200 MB 1009 in 0.56 s, server 28 MiB websockets, max_size=None 200 MB accepted, server 408 MiB websockets, 1 MiB, permessage-deflate 200 MB of zeros, compressed 1009: limit applies after decompression websockets, 1 MiB 200 fragments of 64 KiB 1009 once 1 MiB was read uvicorn default ws_max_size (16 MiB) 10 MB / 20 MB accepted / 1009 uvicorn 1 MiB, 20 clients x 15 MB, websockets-sansio - server peak 60 MiB uvicorn 1 MiB, 20 clients x 15 MB, wsproto - server peak 235 MiB websockets 17.1, uvicorn 0.54, Starlette 1.7, Python 3.14.

2. Remove the limit only with a reason

Turning the limit off makes each connection's memory equal to the largest message any client chooses to send. Measured: with max_size=None, the 200 MB message was accepted and the server peaked at 408 MiB — about twice the message, as frames were buffered and joined. One such client is an outage for a small container; a few at once are an outage for a large one.

MAX_MESSAGE = 256 * 1024          # sized from the largest legitimate message, with headroom

async with serve(handler, "0.0.0.0", 8765, max_size=MAX_MESSAGE):
    await asyncio.Future()

Size the limit from the protocol: if chat messages are at most a few KiB and snapshots at most 200 KiB, a 256 KiB limit leaves headroom without inviting abuse. When a feature genuinely needs large transfers — file uploads over a WebSocket, say — chunk them at the application level, as in step 4, rather than raising the limit for every message.

Verify: the limit is set explicitly in code or configuration, with a comment stating the largest legitimate message.

3. Check that fragments and compression are covered

A message can arrive as many small frames, or compressed. A limit that checked only frame sizes, or only compressed bytes, would miss both. Measured with websockets and a 1 MiB limit: a message sent as 200 fragments of 64 KiB was closed with 1009 and the reason frame with 65536 bytes after reading 1048576 bytes exceeds limit of 1048576 bytes — the library counted the reassembled size. With permessage-deflate negotiated, 200 MB of zeros, which compresses to a tiny fraction of that, was also closed with 1009: the limit applied to the decompressed data.

async with serve(handler, "0.0.0.0", 8765, max_size=2**20, compression="deflate"):
    ...                                   # the limit still bounds the decompressed message

Not every WebSocket implementation does both; test any other stack with fragmented and compressed messages before relying on its limit.

Verify: tests send a fragmented message and a highly compressible message, each above the limit, and both are closed with 1009.

4. Compare implementations under concurrent load

The limit decides which messages are accepted, but not necessarily how much is buffered before the decision. Measured with uvicorn at --ws-max-size 1048576 and 20 clients each sending a 15 MB message at once:

uvicorn app:app --ws websockets-sansio --ws-max-size 1048576   # server peak 60 MiB
uvicorn app:app --ws wsproto --ws-max-size 1048576             # server peak 235 MiB

Both rejected every message. The websockets-sansio implementation peaked at 60 MiB and the client run took 0.80 s; wsproto peaked at 235 MiB and took 1.10 s, buffering far more of the oversized data before closing. With uvicorn's legacy --ws websockets implementation, the 20 MB message test closed with 1009 only after 10.03 s, against 0.04–0.07 s for the others — and uvicorn now warns that this implementation is deprecated in favour of websockets-sansio. Choose the implementation with the same care as the limit, and measure peak memory with many oversized messages at once.

Verify: a load test with concurrent oversized messages keeps server memory within budget for the chosen implementation.

Server peak memory, 20 clients x 15 MB, 1 MiB limit 2 horizontal bars comparing uvicorn --ws websockets-sansio with the others. Server peak memory, 20 clients x 15 MB, 1 MiB limit uvicorn --ws websockets-sansio 60 MiB uvicorn --ws wsproto 235 MiB Both rejected every message; they buffered different amounts first.

5. Chunk large payloads at the application level

When large data must travel over the socket, send it as a sequence of bounded messages with a small header, and let the receiver write each chunk out as it arrives:

CHUNK = 64 * 1024

async def send_file(ws, file_id: str, path: str):
    await ws.send(json.dumps({"type": "begin", "id": file_id, "size": os.path.getsize(path)}))
    with open(path, "rb") as f:
        while chunk := f.read(CHUNK):
            await ws.send(chunk)                     # each message well under max_size
    await ws.send(json.dumps({"type": "end", "id": file_id}))

async def receive_file(ws, out):
    begin = json.loads(await ws.recv())
    if begin["size"] > MAX_UPLOAD:
        await ws.close(code=1009, reason="upload too large")
        return
    received = 0
    while isinstance(message := await ws.recv(), bytes):
        received += len(message)
        if received > begin["size"]:
            await ws.close(code=1008, reason="more data than announced")
            return
        out.write(message)

Each message stays far below the limit, the receiver never holds more than one chunk, and the total is bounded by a separate application limit checked against the announced size. Combine this with back-pressure on the sending side, as in handling WebSocket backpressure with slow consumers.

Verify: a large upload in chunks succeeds with the default message limit, and an upload larger than announced is closed.

Bounding WebSocket input A flow of 4 stages. Bounding WebSocket input max_size from the largest real message Fragments + deflate limit on reassembled, decompressed size Implementation measured memory while rejecting Large data 64 KiB chunks, announced total The message limit is a per-connection memory limit.

Verification

WebSocket message size is bounded when:

  • The limit is explicit at the server — max_size or ws_max_size — and sized from real messages.
  • Messages above it are closed with 1009, including fragmented and compressed ones.
  • Server memory stays bounded under many concurrent oversized messages with the chosen implementation.
  • Large transfers are chunked with an application-level total limit.

Diagnostic Hook: when a WebSocket server's memory jumps while it logs closes with code 1009, check which ASGI WebSocket implementation is running. With a 1 MiB limit and 20 clients sending 15 MB each, wsproto peaked at 235 MiB and websockets-sansio at 60 MiB.

Pitfalls & edge cases

  • max_size=None. Measured: 408 MiB for one 200 MB message.
  • Moving from websockets to uvicorn. The default limit rises from 1 MiB to 16 MiB.
  • Assuming a limit bounds buffering. wsproto buffered up to 235 MiB while rejecting.
  • uvicorn's legacy --ws websockets. Its 1009 close took 10.03 s here, and it is deprecated.

Frequently Asked Questions

What is the default max message size in websockets?

1 MiB (max_size=2**20). A 200 MB message was closed with code 1009 in 0.56 s while the server stayed at 28 MiB.

What is uvicorn's default WebSocket message size limit?

16 MiB (ws_max_size=16777216). A 10 MB message passed and a 20 MB one was closed with 1009. Set --ws-max-size to change it.

Does the WebSocket size limit apply to compressed messages?

In websockets, yes: 200 MB of zeros sent with permessage-deflate was closed with 1009 at a 1 MiB limit, because the decompressed size counts.

What close code means a WebSocket message was too big?

1009, Message Too Big. Both websockets and uvicorn used it, with a reason naming the limit.