asyncio is how Python handles lots of slow, waiting-on-the-network work without threads: it starts many operations, and while each waits for a reply, others run. It's the engine under discord.py (every command handler is async) and the fastest way to write a scraper that fetches hundreds of pages. This guide covers the async patterns that fit those Falix workloads, and the single mistake that stalls the whole thing.
| At a glance | |
|---|---|
| You need | A Falix server running the Python application |
| Plan | Any |
| Time | Twenty minutes |
| Good to already know | Python basics; a running bot or script helps |
async def, await, and the loop
An async def function is a coroutine — calling it doesn't run it, it hands back something you await. await pauses the coroutine until the result is ready, freeing the single thread to run other coroutines meanwhile. An event loop drives them all; asyncio.run() starts one:
import asyncio
async def fetch(name):
await asyncio.sleep(1) # stands in for a network call
return f"done: {name}"
async def main():
result = await fetch("one")
print(result, flush=True)
asyncio.run(main())
🎯 Good to know: On Falix, print with
flush=True(orprint(..., flush=True)) for long-running async apps, so your logs reach the Console immediately instead of sitting in Python's buffer.
Bot-shaped: everything is already async
In discord.py you don't call asyncio.run yourself — the library runs the loop. Your job is to keep handlers async and await the bot's calls:
@bot.tree.command(name="ping", description="Check the bot")
async def ping(interaction: discord.Interaction):
await interaction.response.send_message("Pong!")
The rule that keeps a bot healthy: never block the loop. A blocking call inside a handler freezes the entire bot — it stops answering commands and can drop its Discord connection. See the trap below.
Scraper-shaped: many requests at once
Doing requests one after another wastes almost all your time waiting. asyncio.gather runs them concurrently and collects the results:
import asyncio, aiohttp
async def get(session, url):
async with session.get(url) as resp:
return await resp.text()
async def main():
urls = ["https://example.com", "https://example.org", "https://example.net"]
async with aiohttp.ClientSession() as session:
pages = await asyncio.gather(*(get(session, u) for u in urls))
print(f"fetched {len(pages)} pages", flush=True)
asyncio.run(main())
Ten sequential 1-second requests take ten seconds; ten with gather take about one. (aiohttp or httpx are the async HTTP clients — add one on the Packages page.)
💡 Tip: Don't fire thousands of requests at once — you'll hammer the target and hit rate limits. Cap concurrency with an
asyncio.Semaphore(20)andawaitit inside each task.
The trap: one blocking call freezes everything
Because it's all one thread, a synchronous call that blocks stops every other coroutine dead. The three usual offenders:
| Blocking call | Why it hurts | Use instead |
|---|---|---|
time.sleep(5) |
Freezes the loop for 5s | await asyncio.sleep(5) |
requests.get(...) |
Synchronous HTTP blocks the loop | aiohttp / httpx (async) |
| Heavy CPU work | Nothing else runs while it churns | await asyncio.to_thread(fn, ...) |
If your bot periodically goes unresponsive with no error in the console, a blocking call in an async handler is the first suspect. asyncio.to_thread(...) pushes a blocking or CPU-heavy function onto a worker thread so the loop keeps turning.
Managing tasks
asyncio.create_task(...) starts a coroutine running in the background without waiting for it. Two honest cautions:
- Keep a reference to the task. A task with no reference can be garbage-collected and silently cancelled mid-run.
- Handle its exceptions. An exception in a background task that nobody awaits is easy to miss — wrap the work in
try/exceptand log failures, or the task dies quietly.
task = asyncio.create_task(background_job())
# ... keep `task` alive; later:
await task # surfaces any exception it raised
discord.ext.tasks wraps this pattern for bots (looping jobs like periodic announcements) and handles the reference-keeping for you.
Troubleshooting
- Bot goes silent for seconds at a time — a blocking call on the loop. Swap
time.sleep/requestsfor their async equivalents, or offload withasyncio.to_thread. RuntimeWarning: coroutine '...' was never awaited— you called anasyncfunction withoutawait. Add theawait, orcreate_taskit if it should run in the background.- A background task's error never showed up — you didn't await it or catch its exception. Wrap it and log.
asyncio.run() cannot be called from a running event loop— you're already inside one (e.g. discord.py runs it). Don't callasyncio.runthere; justawaityour coroutine.
The asyncio model has a lot more depth (queues, cancellation, timeouts) — the official Python asyncio docs are the reference.