asyncio
Python
wait_for
synchronous
asynchronous-programming

Python asyncio wait_for synchronous

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

asyncio.wait_for applies a timeout to an awaitable. A synchronous blocking function is not awaitable, so wait_for cannot directly interrupt it just because you wrap the call in async code.

To use wait_for with blocking work, you first have to move that work onto an awaitable boundary. In practice, that usually means running the synchronous function in a thread or process and then awaiting that wrapper.

What wait_for Actually Does

asyncio.wait_for waits for a coroutine, task, or future to complete within a timeout. If the timeout expires first, it cancels the awaitable and raises asyncio.TimeoutError.

That means code like this is valid:

python
1import asyncio
2
3
4async def fetch_data():
5    await asyncio.sleep(2)
6    return "done"
7
8
9async def main():
10    result = await asyncio.wait_for(fetch_data(), timeout=1)
11    print(result)
12
13
14asyncio.run(main())

This times out because fetch_data() is an awaitable coroutine.

Why Synchronous Functions Are Different

Now consider a normal blocking function:

python
1import time
2
3
4def blocking_call():
5    time.sleep(5)
6    return "done"

If you call blocking_call() directly inside async code, it blocks the event loop thread. During that time, asyncio cannot schedule other tasks, and wait_for has nothing to cancel because the blocking work is already monopolizing the thread.

So this is the wrong pattern:

python
async def bad_main():
    result = blocking_call()
    return result

The function is inside an async wrapper, but it is still synchronous work.

The Correct Pattern: Move Blocking Work to a Thread

In modern Python, asyncio.to_thread is usually the simplest solution.

python
1import asyncio
2import time
3
4
5def blocking_call():
6    time.sleep(5)
7    return "done"
8
9
10async def main():
11    try:
12        result = await asyncio.wait_for(
13            asyncio.to_thread(blocking_call),
14            timeout=1,
15        )
16        print(result)
17    except asyncio.TimeoutError:
18        print("Timed out")
19
20
21asyncio.run(main())

Here, asyncio.to_thread(blocking_call) returns an awaitable that represents thread-based execution. wait_for can apply a timeout to that awaitable.

Using run_in_executor on Older Python Versions

If you need compatibility with older Python versions, use loop.run_in_executor.

python
1import asyncio
2import time
3
4
5def blocking_call():
6    time.sleep(5)
7    return "done"
8
9
10async def main():
11    loop = asyncio.get_running_loop()
12
13    try:
14        result = await asyncio.wait_for(
15            loop.run_in_executor(None, blocking_call),
16            timeout=1,
17        )
18        print(result)
19    except asyncio.TimeoutError:
20        print("Timed out")
21
22
23asyncio.run(main())

This is the older but still common pattern.

Important Limitation: Timeout Does Not Stop Native Work Instantly

A subtle but important point is that timing out the awaitable does not forcibly kill arbitrary synchronous code already running in another thread. The awaitable is cancelled from the event loop’s perspective, but the underlying thread function may continue running until it returns.

That means wait_for gives you control over the async caller, not magical preemption of blocking Python or native library code.

If the blocking operation must be truly interruptible, you need a design that supports cooperative cancellation or use a separate process that can be terminated safely.

When to Use a Process Instead of a Thread

Threads are a good fit for I/O-bound blocking functions. For CPU-heavy synchronous work, a process pool is often better because it avoids event-loop blockage and also bypasses the Global Interpreter Lock for pure Python code.

The wrapper pattern is similar, but the execution backend changes.

Common Pitfalls

The most common mistake is thinking that putting blocking code inside async def makes it non-blocking. It does not. Blocking code remains blocking until you move it off the event loop thread.

Another issue is misunderstanding timeout semantics. wait_for times out the await, but it does not guarantee the underlying thread has stopped running.

It is also easy to call asyncio.wait_for(blocking_call(), timeout=1). That fails immediately because blocking_call() runs before wait_for even receives an awaitable.

Finally, use threads only when the blocking function is thread-safe. Some libraries maintain hidden global state and may not behave well under concurrent threaded access.

Summary

  • 'asyncio.wait_for only works with awaitables, not plain synchronous function calls.'
  • Blocking work must be wrapped with asyncio.to_thread or run_in_executor first.
  • Timing out the await does not necessarily terminate the underlying blocking function immediately.
  • Threads are usually good for blocking I/O; processes are better for heavy CPU work.
  • Putting synchronous code inside async def does not make it asynchronous.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.