Python async performance doubts

I'm running an websocket application through Django Channels and with Python 3.12. I have a ping mechanism to check if my users are still connected to my application where at a fixed time interval the fronted sends a ping message to my server (let's say every 5 seconds) and when I receive that message I do the following:

I call await asyncio.sleep(time interval * 2) ;

Afterwards I check if the timestamp of the last message I received in this handler or in any other of the ones that is active is higher than time_interval * 1.5 and if it is I disconnect the user in order to free that connection resources since it isn't no longer active;

However we noticed that this sleep call we make every X seconds for each active connection that we have when a ping call is received may be causing some performance issues on the application as an all. We are assuming this because we noticed that every time we shut down this ping logic we need less resources (number of pods and CPU) to handle the median number of traffic we have then when we have this ping handler active. In other hand it seems that all the messages we serve through django channel also flow very much faster when this ping handler is disconnected.

My handler:

if action == "ping":
    await self.ping_handler(message_info)
    async def ping_handler(self, message_info):

        await asyncio.sleep(PING_INTERVAL * 2)
        if (
            round_half_up(
                (dt.datetime.now() - self.last_message).total_seconds(), decimals=0
            )
            >= PING_INTERVAL * 1.5
        ):
              await self.close()

Besides this ping handler what my application does is receiving messages from clients, processes some of that messages data and then broadcast it to the appropriate channels like this:

async def action_dispatcher(self, action, message_info):
    try:
        if action == "post-message":
            await self.handle_message(message_info)
        elif action == "post-username":
            await self.change_nickname()
        elif action == "post-change-privacy":
            await self.handle_change_privacy(message_info)
        elif action == "get-bet-status":
            await self.get_bet_status(message_info)
        elif action == "ping":
            self.last_message = dt.datetime.now()
            await self.ping_handler(message_info)


async def handle_message(message_info):

     # execute business logic before these steps

        encoded_message = await super().encode_socket_message(
            [
                "message_type",
                self.nickname,
                "success",
            ]
        )
        await self.channel_layer.group_send(
            self.username_group,
            encoded_message,
        )

Can someone help me understanding if my assumptions are right or wrong? I searched a lot around the internet and ChatGPT but I couldn't find a legitimate answer to my doubts that if we have 200 to 500 active connections in a channels application that every 5 seconds execute this ping logic that it could cause performance limitations.

The purpose of my application is to receive chat messages from clients, process their content and the broadcast it to the rest of the room through django channel layers.

I tried running my application with and without this ping handling logic and searched through foruns,stackoverflow questions and chat gpt to find aswers.

That the performance is worse is not surprising. Django Channels are first-in first out (FIFO) queues [source].

The action_dispatcher processes messages serially, and so by waiting for X seconds in the dispatch queue you are preventing any other messages from being processed on that queue for X seconds.

If you want to not block message processing, then you should create a task. The task will run concurrently with your action_dispatcher.

eg.

async def action_dispatcher(self, action, message_info):
    try:
        if action == "post-message":
            ...
        elif action == "ping":
            self.last_message = dt.datetime.now()
            task = asyncio.create_task(self.ping_handler(message_info))
            # NB. Do not await this task in your action dispatcher
            # NB2. Make sure to keep a reference to your task available, otherwise 
            # it may not run to completion. eg.
            self.ping_tasks.append(task)

If you just do this, then you may get warnings about tasks that have not had their results awaited. You might be able to use a TaskGroup to make sure these child tasks are handled properly. That is, keeping a reference until they are finished and making sure their results are collected. Make a TaskGroup instance available to the action_dispatcher() and then do task_group.create_task(self.ping_handler(message_info))` instead.

Back to Top