You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@aaugustin
Suppose a client sends 100 concurrent requests in a format {'id$i': $i}. Server processes (runs some operations) all the individual requests, each request may take a different amount of time.
The following code throws an error - RuntimeError: cannot call recv while another coroutine is already waiting for the next message. Why can we not let the user handle the order of the recv messages?
recv_tasks= []
foriinrange(100):
awaitwebsocket.send(f'{{id{i}: {i}}}')
recv_tasks.append(
asyncio.create_task(websocket.recv())
)
forresponseinasyncio.as_completed(recv_tasks):
awaitresponse# check id of the response & process
The main reason is that it leads to non deterministic behavior without bringing any benefit in terms concurrency, as the recv() calls will be serialized anyway. Most people hitting this error realize they made an error in their logic.
This should have pretty much the same performance characteristics and is easier to understand IMO:
foriinrange(100):
awaitwebsocket.send(f'{{id{i}: {i}}}')
foriinrange(100):
response=awaitwebsocket.recv()
# check id of the response & process
If you really want to start reading responses before you're finished sending requests, you can run these two loops in separate tasks.
This kind of breaks a fairly beneficial asyncio property for cases where you don't need the connection to be "in lock step."
If you have some code:
while True:
input = await websocket.recv()
result = await process_the_request_which_may_take_a_while(input)
await websocket.send(result)
You should be able to prevent recvs from being blocked by your processing loop by just doing:
async with asyncio.TaskGroup() as tg:
for _ in range(os.cpu_count()):
async def _run_my_loop():
while True:
input = await websocket.recv()
result = await process_the_request_which_may_take_a_while(input)
await websocket.send(result)
tg.create_task(_run_my_loop())
Without that, making your send and receive loop parallel is much more awkward (producer & consumer loops and code to connect input -> processing -> output is required).
You kind of need a messaging service and a producer & consumer loop for "applications at scale" anyways, but it's a shame that you can't simply have N number of loops to process up to N number of things in parallel.
I guess for anyone hitting this exact situation, here's a rough sketch of the code you need to actually bridge the gap (call it MIT license):
# This was written to be compatible with 3.6, so the typing is a bit dated.fromtypingimport (
Generic,
TypeVar
)
InputType=TypeVar('InputType')
OutputType=TypeVar('OutputType')
classProducerConsumerQueue(Generic[InputType, OutputType]):
def__init__(self) ->None:
self._input_event=asyncio.Event()
self._output_event=asyncio.Event()
self._input_queue: List[InputType] = []
self._output_queue: List[OutputType] = []
defsubmit_work(self, input: InputType) ->None:
self._input_queue.append(input)
self._input_event.set()
asyncdefget_work(self) ->InputType:
whileTrue:
awaitself._input_event.wait()
iflen(self._input_queue) ==0:
continueiflen(self._input_queue) ==1:
self._input_event.clear()
returnself._input_queue.pop(0)
defsubmit_result(self, result: OutputType) ->None:
self._output_queue.append(result)
self._output_event.set()
asyncdefget_result(self) ->OutputType:
whileTrue:
awaitself._output_event.wait()
iflen(self._output_queue) ==0:
continueiflen(self._output_queue) ==1:
self._output_event.clear()
returnself._output_queue.pop(0)
my_queue=ProducerConsumerQueue()
asyncdefrun_input():
whileTrue:
input=awaitwebsocket.recv()
my_queue.submit_work(input)
asyncdefrun_work():
for_inrange(os.cpu_count()):
asyncdef_run_my_loop():
whileTrue:
awaitmy_queue.get_work()
result=awaitprocess_the_request_which_may_take_a_while(input)
my_queue.submit_result(result)
tg.create_task(_run_my_loop())
asyncdefrun_output():
whileTrue:
result=awaitmy_queue.get_result()
awaitwebsocket.send(result)
If you want back pressure, you'll need to tweak that so that you check the size of the input queue before adding more work.
Thanks, I'd missed that there was already a asyncio.Queue library type; that certainly makes things easier (and it looks like it also has a builtin -- optional -- queue size limit, which if used, handles the back pressure side of things as well).
It's also good to know the limitation on websockets is expected only on recv side.
I opened #1656 so that others might be (slightly) more likely to benefit from this discussion.
Indeed, send can be called concurrently safely. websockets serializes concurrent calls.
In the standard case, send yields control to the event loop only when the buffer is full. In that case, it's good to serialize so that the second call to send wait for the buffer to drain; writing earlier would be pointless; also it would break backpressure.
In the case of a fragmented message where fragments are generated asynchronously (not what you're doing here), serializing writes is necessary because the WebSocket protocol doesn't support interleaving messages.
Activity
aaugustin commentedon Jul 28, 2020
Let's take a simpler example. Let's say you have a TCP stream and a coroutine that reads exactly one byte from the stream.
Now, let's say you run:
The stream receives
"abc". The three coroutines can read one byte each.What do you expect the example to return?
["a", "b", "c"]?["c", "b"', "a"]? Any other permutation?All these would be equally valid.
The semantics are non-deterministic. I believe this is bad.
Now, if you replace
stream.read()withconn.recv(), it's exactly the same situation, and that's why websockets prohibits concurrent calls torecv().deepankarm commentedon Dec 30, 2020
@aaugustin
Suppose a client sends 100 concurrent requests in a format
{'id$i': $i}. Server processes (runs some operations) all the individual requests, each request may take a different amount of time.The following code throws an error -
RuntimeError: cannot call recv while another coroutine is already waiting for the next message. Why can we not let the user handle the order of therecvmessages?aaugustin commentedon Dec 30, 2020
The main reason is that it leads to non deterministic behavior without bringing any benefit in terms concurrency, as the
recv()calls will be serialized anyway. Most people hitting this error realize they made an error in their logic.This should have pretty much the same performance characteristics and is easier to understand IMO:
If you really want to start reading responses before you're finished sending requests, you can run these two loops in separate tasks.
DarkArc commentedon Sep 4, 2025
This kind of breaks a fairly beneficial asyncio property for cases where you don't need the connection to be "in lock step."
If you have some code:
You should be able to prevent recvs from being blocked by your processing loop by just doing:
Without that, making your send and receive loop parallel is much more awkward (producer & consumer loops and code to connect input -> processing -> output is required).
You kind of need a messaging service and a producer & consumer loop for "applications at scale" anyways, but it's a shame that you can't simply have N number of loops to process up to N number of things in parallel.
I guess for anyone hitting this exact situation, here's a rough sketch of the code you need to actually bridge the gap (call it MIT license):
If you want back pressure, you'll need to tweak that so that you check the size of the input queue before adding more work.
aaugustin commentedon Sep 5, 2025
Yes, using a queue is the general idea, although you could make the implementation much simpler:
asyncio.Queueinstead of anasyncio.Eventand alist.Here's what it should look like (untested; expect stupid mistakes):
DarkArc commentedon Sep 5, 2025
Thanks, I'd missed that there was already a
asyncio.Queuelibrary type; that certainly makes things easier (and it looks like it also has a builtin -- optional -- queue size limit, which if used, handles the back pressure side of things as well).It's also good to know the limitation on
websocketsis expected only onrecvside.I opened #1656 so that others might be (slightly) more likely to benefit from this discussion.
aaugustin commentedon Sep 5, 2025
Indeed,
sendcan be called concurrently safely. websockets serializes concurrent calls.sendyields control to the event loop only when the buffer is full. In that case, it's good to serialize so that the second call tosendwait for the buffer to drain; writing earlier would be pointless; also it would break backpressure.(More info than you wanted, I guess!)