Skip to content

Conversation

@broddo
Copy link

@broddo broddo commented Feb 11, 2026

I'm working on a project that has moderate websocket data requirements. Everything works great on MacOS/Linux even at high data rates (60 msgs/sec, 120 bytes/msg). But I found that Windows clients struggled with even the lightest data rates. Just a couple of messages per second would start causing "Too many messages queued" (if I removed the availableForWriteAll() calls) or dropped messages otherwise.

It looks like the issue is that the previous _runQueue() implementation was only sending one message per invocation, then waiting for the next event loop tick or TCP ACK callback to continue. This created a bottleneck where the message queue would fill faster than messages could be drained, especially on Windows clients which appear to have different TCP ACK timing characteristics. Coupled with that is Windows tendency to send ACKs in bursts.

I've modified _runQueue(). Instead of sending one message and returning, it iterates through all queued messages and sends as much data as the TCP window allows in a single pass. _onAck() now distributes ACKs across multiple messages.

This seems to have fixed Windows clients for me at least. They're now able to keep up with MacOS/Linux clients.

@mathieucarbou
Copy link
Member

Hi @broddo ,
That's a good point and might also be linked to some user reports I've seen where people were saying that they quickly see messages dequeuing.
I was not able to reproduce that myself, being on MacOS, and I think that's the same for other team members.
Your optimization makes sense.
I will as the team to review.

Copy link
Member

@mathieucarbou mathieucarbou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is my review.
Thank you for the PR !

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@mathieucarbou
Copy link
Member

mathieucarbou commented Feb 11, 2026

@broddo @willmmiles @me-no-dev @vortigont FYI I was able to test on MacOS and this improvement also increases the throughput a lot.

I am testing with WebSocket.ino + Safari (what is often causing issues or limitations)

by changing inside: static uint32_t deltaWS = 100;

In main we have an issue when using Safari: when we hit refresh page sevral times or we send a message from the txtfield too frequently, the message queue starts to overflow.

With the new implementation safari receives all the messages fine, we can refresh the page without issue (which also causes a fragmented pong control frame) and we can also send a lot of messages from the UI without impacting the queue.

Just for fun, I tested also with static uint32_t deltaWS = 50;: we were not able to go below 100ms delay before : too much load and queue was overflowing.

In this test, since messages are short, we can go as low as 50ms delay without any issue and queue overflow !!

That's very good !

So a big improvement of throughput for short messages, which makes sense since more messages in the same tcp buffer.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mathieucarbou mathieucarbou force-pushed the websocket-windows-throughput branch 4 times, most recently from b36fd82 to 3457302 Compare February 12, 2026 11:43
broddo and others added 3 commits February 12, 2026 12:43
Enable processing of cumulative  ACKs across multiple queued messages

Larger window size for reduced fragmentation

Update src/AsyncWebSocket.cpp

Co-authored-by: Mathieu Carbou <mathieu.carbou@gmail.com>

Revert threshold to 9

improve readability

Fix loop exit condition

Co-authored-by: Mathieu Carbou <mathieu.carbou@gmail.com>

Replace public sent() with private _remainingBytesToSend()

Leverage _remainingBytesToSend() and tidy messageQueue sending
- Increase throughput of WS example
- Remove \n at the end of log line
- Fix status that was incorrectly kept to 0 even when all bytes acked
- Added traces in sent method
@mathieucarbou mathieucarbou force-pushed the websocket-windows-throughput branch from 3457302 to a3a1466 Compare February 12, 2026 11:43
@mathieucarbou
Copy link
Member

mathieucarbou commented Feb 12, 2026

@broddo : I added some more traces this morning and did some further testing following the inclusion of my review in your code and I found an issue in a fix I suggested. So:

  • I've rebased on main
  • I've squashed all your commits into one
  • I've added a PR Review & Test commit to add more traces, update the WS example and fix the ack method which was incorrectly setting the status flag
  • I've added another commit to also fix the runQueue method as we discussed to prioritize control frames

I did some extensive test again with several clients spawned with:

websocat --text --ping-interval 2 ws://192.168.4.1/ws > /dev/null

in order to cause some ping-pong control frames mixed with websocket messages.

No memory leak : when disconnecting clients, heap recovers like before running the test after a while. And thats is uper fast now... No queue exhaustion!

That's the sort of logs we can see when activating verbose mode:

 40543][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 8/8, acked: 0/0
[ 40552][V][AsyncWebSocket.cpp:227] send(): C[56901] Sent 8/8, ack: 0/10, final: 1
[ 40560][V][AsyncWebSocket.cpp:400] _runQueue(): WS[2] Send message fragment: 8/8, acked: 0/0
[ 40569][V][AsyncWebSocket.cpp:227] send(): C[56903] Sent 8/8, ack: 0/10, final: 1
[ 40606][V][AsyncWebSocket.cpp:174] ack(): msg code: 1, ack: 10/10, remain=0/10, status: 1
[ 40627][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 8/8, acked: 0/0
[ 40636][V][AsyncWebSocket.cpp:227] send(): C[56901] Sent 8/8, ack: 0/10, final: 1
[ 40644][V][AsyncWebSocket.cpp:400] _runQueue(): WS[2] Send message fragment: 8/8, acked: 0/0
[ 40653][V][AsyncWebSocket.cpp:227] send(): C[56903] Sent 8/8, ack: 0/10, final: 1
[ 40679][V][AsyncWebSocket.cpp:174] ack(): msg code: 1, ack: 10/10, remain=8/10, status: 1
[ 40687][V][AsyncWebSocket.cpp:174] ack(): msg code: 1, ack: 8/10, remain=0/8, status: 0
[ 40695][V][AsyncWebSocket.cpp:174] ack(): msg code: 1, ack: 10/10, remain=0/10, status: 1
[ 40703][V][AsyncWebSocket.cpp:174] ack(): msg code: 1, ack: 10/10, remain=8/10, status: 1
[ 40711][V][AsyncWebSocket.cpp:174] ack(): msg code: 1, ack: 8/10, remain=0/8, status: 0

@mathieucarbou mathieucarbou merged commit 25b0628 into ESP32Async:main Feb 12, 2026
33 checks passed
@mathieucarbou
Copy link
Member

@broddo : PR merged!

Could you please now test your code by pointing to main branch ? It contains also several latest changes.

Thanks!

Note: I edited also the WebSocket example to send each 5 sec a big message of 8k characters in order to test fragmentation from server to client.

websocat --text --ping-interval 2 ws://192.168.4.1/ws

We can see below the message being sent in fragments, plus after that short messages each 50ms, then the ack gets received. In the middle of all that, we have some ping-pong control frames.

[ 10008][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 8192/8192, acked: 0/0
[ 10019][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 5736/8192, final: 0, acked: 0/5740
Free heap: 195476
[ 10406][V][AsyncWebSocket.cpp:568] _onData(): WS[1]: _onData: plen=18, _pstate=0, _status=1
[ 10414][V][AsyncWebSocket.cpp:595] _onData(): WS[1]: _pinfo: index: 0, final: 1, opcode: 9, masked: 1, len: 12
[ 10424][V][AsyncWebSocket.cpp:635] _onData(): WS[1]: mask read complete
[ 10431][V][AsyncWebSocket.cpp:691] _onData(): WS[1]: processing final fragment index=0, len=12
[ 10439][V][AsyncWebSocket.cpp:717] _onData(): WS[1]: processing ping
[ 10573][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 1436/5740, left: 0/1436, status: 0
[ 10582][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 2456/8192, acked: 1436/5740
[ 10593][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 8192/8192, final: 1, acked: 1436/8200
[ 10602][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10611][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10618][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10627][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10635][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10644][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10651][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10660][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10668][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10677][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10684][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10693][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10701][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10710][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10718][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10726][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10734][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10743][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10751][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10760][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10767][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10776][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10784][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 4308/8200, left: 0/2872, status: 0
[ 10793][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 5740/8200, left: 0/1432, status: 0
[ 10802][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10811][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10869][V][AsyncWebSocket.cpp:400] _runQueue(): WS[1] Send message fragment: 9/9, acked: 0/0
[ 10878][V][AsyncWebSocket.cpp:227] send(): C[61090] sent: 9/9, final: 1, acked: 0/11
[ 10909][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 8200/8200, left: 143/2603, status: 1
[ 10918][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 132/143, status: 1
[ 10926][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 121/132, status: 1
[ 10934][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 110/121, status: 1
[ 10943][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 99/110, status: 1
[ 10951][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 88/99, status: 1
[ 10959][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 77/88, status: 1
[ 10967][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 66/77, status: 1
[ 10975][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 55/66, status: 1
[ 10983][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 44/55, status: 1
[ 10991][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 33/44, status: 1
[ 10999][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 22/33, status: 1
[ 11007][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 11/22, status: 1
[ 11016][V][AsyncWebSocket.cpp:174] ack(): opcode: 1, acked: 11/11, left: 0/11, status: 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants