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¶
- websockets or an ASGI server with WebSocket support.
- WebSockets in Starlette, from handling WebSockets in FastAPI and Starlette.
- The topic overview, WebSocket & Real-Time Streams.
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.
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.
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.
Verification¶
WebSocket message size is bounded when:
- The limit is explicit at the server —
max_sizeorws_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.
Related¶
- WebSocket & Real-Time Streams — up to the topic overview.
- Sending binary and msgpack over WebSockets — smaller messages for the same data.
- Network I/O & Protocol Handling — the section overview.