ENGINEERING NOTE

How one blocking call froze my Discord bot

While the local model was thinking, my bot could not answer anyone else. The cause was one ordinary HTTP request inside an async function.

The first version of my Discord bot worked, as long as only one person used it. While the local Qwen model was generating a reply, which can take many seconds on my machine, the bot could not respond to anything else.

Why async code can still block

discord.py runs on asyncio: one thread and one event loop that switches between tasks whenever a task hits an await. That only works if every slow operation actually awaits. requests.post does not. It is a normal blocking call, so while it waited for the model, the event loop could not run anything else, including the heartbeat that keeps the bot connected to Discord (discord.py logs a “heartbeat blocked” warning when this happens).

The fix

discord_ai_bridge/main.py
# Before: blocks the event loop until the model answers
response = requests.post(API_URL, json=payload)

# After: runs in a worker thread; the event loop keeps serving others
response = await asyncio.to_thread(requests.post, API_URL, json=payload)

asyncio.to_thread hands the call to a thread pool and gives the event loop an awaitable. The bot keeps reading messages and sending heartbeats while the model works.

Why I did not switch to aiohttp

An async HTTP client would avoid threads entirely, and it is the cleaner design for a bot under real load. I chose to_thread because it fixed the bug with one line in code I already understood, and the bot serves one small server. The trade-off is a thread per in-flight request and no way to cancel one once it starts.

What is still wrong

  • There is no timeout= on the request. If LM Studio hangs, that worker thread waits forever. Adding timeout=120 and catching requests.exceptions.Timeout is the next change.
  • Discord rejects messages over 2,000 characters, and long model answers will fail to send. Replies need to be split into chunks.

The lesson I took from it: in async code, “it works” is not enough. The question is whether it still works while something slow is happening.