Blocking calls inside async code

Concurrency in Python services Mid-level Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Fill the blank

Keep the event loop free while a blocking SDK call runs.

Python 3.13
import asyncio
import time


def legacy_sdk() -> str:
    time.sleep(0.2)
    return "ok"


async def main() -> None:
    result = await ____
    print(result)


asyncio.run(main())

Options
Hint 1 of 1

Which helper runs a sync function off the loop?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A asyncio.to_thread(legacy_sdk)

Correct. The call runs in a worker thread and the loop stays responsive.

A blocking call like time.sleep stops the whole event loop and every other request on it. asyncio.to_thread runs the function in a worker thread and gives back an awaitable, so the loop keeps serving while it waits.

Why not the others?
  • B legacy_sdk(). Awaiting a str fails, and the call already blocked the loop.
  • C asyncio.sleep(legacy_sdk()). Runs the blocking call first, on the loop.
  • D asyncio.create_task(legacy_sdk). create_task needs a coroutine, not a plain function.

30-second answer

A synchronous call inside a coroutine blocks the event loop thread, so every other request on that loop waits until it returns. Fix it by using an async client, or by moving the call to a thread with await asyncio.to_thread(fn, ...) (or loop.run_in_executor), which frees the loop while the thread waits. CPU-heavy work should go to a process pool instead. asyncio's debug mode reports callbacks that take longer than 100 ms.

Practise saying it · 30 s. No recording, just a timer

Why not block in async code?

Say your answer out loud, then check which of these points you covered:

  • One loop thread
  • Blocking stalls every request
  • Async clients or to_thread
  • Process pool for CPU
  • Debug mode finds it

Deep dive

PYTHONASYNCIODEBUG=1 or asyncio.run(main(), debug=True) logs slow callbacks (slow_callback_duration, default 0.1 s).

Thread pools have limited size; many slow blocking calls still queue up.

Common culprits: requests, boto3, time.sleep, sync database drivers, large JSON parsing.

Frameworks like FastAPI run def (non-async) endpoints in a thread pool automatically.

Common mistakes

  • time.sleep in a coroutine.
  • A sync database driver inside async handlers.
  • Wrapping CPU-heavy work in to_thread and expecting parallelism.

Senior take & follow-ups

Explain why one blocking call stalls everything, then async clients, to_thread, and process pools for CPU.

Follow-up questions

Think of your answer first, then open the hint.

  1. What does to_thread do?

    Show hint

    Runs the function in the default thread pool and awaits its result.

  2. How do you find blocking calls?

    Show hint

    asyncio debug mode, profilers, and latency spikes correlated with one endpoint.

Suggest a fix if something here is wrong or out of date.