Last Updated on December 15, 2023
You can and should add timeouts to long-running tasks in asyncio programs.
In this tutorial, you will discover the importance of timeouts in asyncio and how to add timeouts to your programs.
Let’s get started.
What is a Timeout?
In asyncio, a timeout refers to a mechanism for setting a maximum allowable duration for an asynchronous operation to complete.
If the operation does not complete within the specified time frame, asyncio raises a timeout exception.
A specified period of time that will be allowed to elapse in a system before a specified event is to take place, unless another specified event occurs first; in either case, the period is terminated when either event takes place.
— Timeout (computing), Wikipedia.
Timeouts are used to ensure that asynchronous tasks or coroutines do not block indefinitely and to provide a way to handle situations where an operation takes longer than expected.
There are 3 key components of a timeout in asyncio:
- Duration: A timeout is defined by a duration, which is the maximum amount of time, usually specified in seconds, that the asynchronous operation is allowed to take.
- Timeout Exception: If the operation does not complete within the specified duration, asyncio typically raises a asyncio.exceptions.TimeoutError exception. This exception can be caught and handled by the application to implement appropriate error handling or recovery logic.
- Timeout Handling: asyncio provides various mechanisms for setting and handling timeouts, such as a function argument, a context manager, and potentially cancellation of the target task.
Timeouts are essential in asyncio programming to ensure that the program remains responsive, avoids resource exhaustion, and provides graceful error handling for scenarios where asynchronous operations take longer than expected to complete.
Run loops using all CPUs, download your FREE book to learn how.
Why Use Timeouts
Using timeouts in asyncio is important for several reasons, such as:
- Preventing Resource Exhaustion: Without timeouts, a misbehaving or slow coroutine could potentially occupy a significant amount of resources (e.g., CPU time, memory, network connections). Timeouts ensure that if a coroutine takes too long to complete, it can be interrupted, preventing resource exhaustion.
- Responsiveness: Timeouts help maintain the responsiveness of your asynchronous application. For example, if you’re waiting for a network request to complete, you don’t want to wait indefinitely; you want to set a timeout so that if the request takes too long, your program can react accordingly.
- Graceful Handling of Failures: Timeouts provide a way to gracefully handle situations where an operation is expected to complete within a certain time frame but fails to do so. Instead of waiting indefinitely, you can catch a timeout exception and handle it appropriately.
- Avoiding Deadlocks: In scenarios where multiple coroutines are involved in synchronization or communication, timeouts can help prevent deadlocks. If a coroutine is waiting for a signal or response and it doesn’t arrive within a reasonable time, a timeout can break the deadlock.
- Fault Tolerance: When dealing with external services or resources, it’s important to have fault-tolerant behavior. Timeouts allow you to handle cases where an external resource becomes unavailable or responds slowly.
I might go further and say making use of timeouts for long-running tasks in asyncio is a best practice.
When To Use Timeouts
In asyncio programs, you should use timeouts in the following scenarios:
- Blocking or Slow Operations: When an asynchronous operation has the potential to block for an extended period or is known to be slow, you should consider setting a timeout. This prevents the operation from causing the entire program to hang indefinitely.
- External Resource Access: When interacting with external resources like databases, web services, or APIs, consider setting timeouts to ensure that your program doesn’t wait indefinitely for a response that may never arrive.
- Concurrency Control: In scenarios involving multiple concurrent tasks or coroutines, you can use timeouts to prevent deadlocks or to ensure that certain tasks do not monopolize resources for too long.
- Resource Management: Timeouts can be used for resource management, such as releasing resources (e.g., file handles, network connections) if they are not used within a certain time frame.
- Task Coordination: In cases where you need to coordinate tasks or coroutines, timeouts can be used to ensure that tasks do not wait indefinitely for synchronization points or responses from other tasks.
Generally, I would recommend using timeouts any time your system touches an external system or resource over which you don’t have full control.
Free Python Asyncio Course
Download your FREE Asyncio PDF cheat sheet and get BONUS access to my free 7-day crash course on the Asyncio API.
Discover how to use the Python asyncio module including how to define, create, and run new coroutines and how to use non-blocking I/O.
How to Use Timeout
There are perhaps 5 ways to use a timeout in asyncio programs, they are:
- Add a timeout with asyncio.wait()
- Add a timeout with asyncio.wait_for()
- Add a timeout with asyncio.as_completed()
- Add a timeout with asyncio.timeout()
- Add a timeout with asyncio.timeout_at()
Let’s take a closer look at each in turn.
Add a Timeout with asyncio.wait()
The asyncio.wait() call is a coroutine.
It takes a collection of asyncio tasks as an argument and returns when a condition on the provided tasks is met, such as all tasks are done, one task is done, or one task has failed.
On completion, asyncio.wait() will return two sets, the first containing those provided tasks that match the condition (e.g. are done) and those remaining tasks that do not.
For example:
1 2 3 |
... # wait for all tasks to complete done, pending = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) |
A timeout can be specified to the asyncio.wait() coroutine via a “timeout” argument in seconds.
timeout (a float or int), if specified, can be used to control the maximum number of seconds to wait before returning.
— Coroutines and Tasks
For example:
1 2 3 |
... # wait for all tasks to complete done,pending = await asyncio.wait(tasks, timeout=5) |
The timeout number of seconds may also be a float, providing fractions of a second.
For example:
1 2 3 |
... # wait for all tasks to complete done,pending = await asyncio.wait(tasks, timeout=0.01) |
If the call does not return within the timeout number of elapsed seconds, then the call will return.
If the call times out, a TimeoutError exception will not be raised and the target tasks will not be cancelled.
Note that this function does not raise TimeoutError. Futures or Tasks that aren’t done when the timeout occurs are simply returned in the second set. […] Unlike wait_for(), wait() does not cancel the futures when a timeout occurs.
— Coroutines and Tasks
- Does not raise a TimeoutError exception on timeout.
- Does not cancel target tasks.
By default, no timeout is used, allowing the call to asyncio.wait() to wait for as long as is needed.
You can learn more about how to use asyncio.wait() in the tutorial:
Add a Timeout with asyncio.wait_for() (With Cancellation)
The asyncio.wait_for() call is a coroutine.
This coroutine is intended to execute a single task with a limited timeout.
It takes an awaitable, such as a coroutine or task and a timeout in seconds. It will return when the provided task is complete or the timeout has elapsed.
A timeout can be specified to the asyncio.wait_for() as either None (no timeout), an integer, or a floating point number of seconds.
timeout can either be None or a float or int number of seconds to wait for. If timeout is None, block until the future completes.
— Coroutines and Tasks
The timeout number of seconds can be an integer.
For example:
1 2 3 |
... # wait for the task with an integer timeout await asyncio.wait_for(task(), timeout=5) |
The timeout number of seconds may also be a float, providing fractions of a second.
For example:
1 2 3 |
... # wait for the task with an integer timeout await asyncio.wait_for(task(), timeout=2.01) |
If the call does not return within the timeout number of elapsed seconds, then the target task is cancelled and a TimeoutError exception is raised.
If a timeout occurs, it cancels the task and raises TimeoutError.
— Coroutines and Tasks
Therefore, it is good practice to wrap the call to asyncio.wait_for() with a try-except for the TimeoutError exception.
If some other task cancels the target task while it is running, a asyncio.CancelledError exception may be raised. Therefore, we may want to handle the asyncio.CancelledError exception.
Finally, if a the task itself fails and raises an exception, this will be propagated back to the caller if the result of the task is retrieved. Therefore, any expected exceptions should be handled.
For example, a robust call to asyncio.wait_for() with a timeout may look as follows:
1 2 3 4 5 6 7 8 9 10 11 12 |
# handle timeout try: # wait for the task for a fixed interval result = await asyncio.wait_for(long_running_task, timeout=5) # handle result # ... except asyncio.TimeoutError: # log timeout except asyncio.CancelledError: # log some other task canceled it except Exception as ex: # log failure |
Note, replace the “except Exception” with specific exceptions raised in the task. It’s not a great idea to catch the base class Exception.
You can learn more about asyncio.wait_for() in the tutorial:
Add a Timeout with asyncio.as_completed()
The asyncio.as_completed() call is a coroutine.
It takes a collection of coroutines or tasks and yields those tasks one at a time in the order that they are done.
For example:
1 2 3 4 5 6 7 |
... # get tasks in the order they are done for task in asyncio.as_completed(tasks): # get the result from the next task that is done result = await task # handle the result # ... |
The asyncio.as_completed() takes a “timeout” argument in seconds. It specifies how long the caller is willing to wait for all provided tasks to be done, that is for the iteration of all tasks.
The timeout can be specified as an integer number of seconds.
For example:
1 2 3 4 5 6 7 |
... # get tasks in the order they are done for task in asyncio.as_completed(tasks, timeout=10): # get the result from the next task that is done result = await task # handle the result # ... |
The timeout can also be specified as a floating point value with fractions of seconds.
For example:
1 2 3 4 5 6 7 |
... # get tasks in the order they are done for task in asyncio.as_completed(tasks, timeout=5.5): # get the result from the next task that is done result = await task # handle the result # ... |
By default, no timeout is used.
If all tasks are not done within the given timeout, then a asyncio.TimeoutError exception is raised.
Raises TimeoutError if the timeout occurs before all Futures are done.
— Coroutines and Tasks
As such, this exception should be handled when using a timeout.
If some other task cancels one of the tasks in the provided collection, an asyncio.CancelledError exception may be raised. Therefore, we may want to handle the asyncio.CancelledError exception.
Finally, if tasks themselves fail and raises an exception, this will be propagated back to the caller if the result of a task is retrieved. Therefore, any expected exceptions should be handled.
For example, a robust call to asyncio.as_completed() with a timeout may look as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... # handle a timeout try: # get tasks in the order they are done for task in asyncio.as_completed(tasks, timeout=10): # get the result from the next task that is done result = await task # handle the result # ... except asyncio.TimeoutError: # log timeout except asyncio.CancelledError: # log some other task canceled a task except Exception as ex: # log failure |
Note, replace the “except Exception” with specific exceptions raised in the task. It’s not a great idea to catch the base class Exception.
You can learn more about how to use asyncio.as_completed() in the tutorial:
Add a Timeout with asyncio.timeout() (With Cancellation)
The asyncio.timeout() call is an asynchronous context manager.
As such, it must be used with the “async with” expression.
The asyncio.timeout() context manager allows one or more tasks to run within the body with a fixed timeout.
It takes a “delay” argument in seconds.
delay can either be None, or a float/int number of seconds to wait. If delay is None, no time limit will be applied; this can be useful if the delay is unknown when the context manager is created.
— Coroutines and Tasks
This may be an integer value.
For example:
1 2 3 4 5 6 7 |
... # run tasks with a timeout async with asyncio.timeout(5): # execute long running task result = await long_running_task() # process result # ... |
The delay may also be a floating point value allowing fractions of second.
For example:
1 2 3 4 5 6 7 |
... # run tasks with a timeout async with asyncio.timeout(0.1): # execute long running task result = await long_running_task() # process result # ... |
If the body of the context manager takes longer than the provided delay, the current task that is running is canceled and a asyncio.TimeoutError exception is raised.
… the context manager will cancel the current task and handle the resulting asyncio.CancelledError internally, transforming it into a TimeoutError which can be caught and handled.
— Coroutines and Tasks
As such, this exception should be handled.
If some other task cancels the running task, an asyncio.CancelledError exception may be raised. Therefore, we may want to handle the asyncio.CancelledError exception.
Finally, if a task itself fails and raises an exception, this will be propagated back to the caller if the result of a task is retrieved. Therefore, any expected exceptions should be handled.
For example, a robust call to asyncio.timeout() may look as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... # handle exceptions try: # run tasks with a timeout async with asyncio.timeout(5): # execute long running task result = await long_running_task() # process result # ... except asyncio.TimeoutError: # log timeout, task was canceled by timeout except asyncio.CancelledError: # log some other task canceled it except Exception as ex: # log failure |
Note, replace the “except Exception” with specific exceptions raised in the task. It’s not a great idea to catch the base class Exception.
You can learn more about how to use the asyncio.timeout() context manager in the tutorial:
Add a Timeout with asyncio.timeout_at()
The asyncio.timeout_at() call is an asynchronous context manager.
As such, it must be used with the “async with” expression.
The asyncio.timeout_at() context manager allows one or more tasks to run within the body with a fixed timeout.
Unlike asyncio.timeout() that takes a delay in seconds into the future from the time of the call, the asyncio.timeout_at() context manager takes a time in the future, relative to the event loop time.
The argument to asyncio.timeout_at() is “when“, an absolute time in the future.
when is the absolute time to stop waiting, or None.
— Coroutines and Tasks
A “when” time can be calculated by retrieving the current event loop time and adding a number of seconds to it, as either an integer or floating point value.
The current event loop can be retrieved via the asyncio.get_running_loop() function.
For example:
1 2 3 |
... # get the event loop object loop = asyncio.get_running_loop() |
The current event loop time can be retrieved by calling the time() method on the event loop object.
For example:
1 2 3 |
... # get the current event loop time current_time = loop.time() |
A future time for the “when” argument can be calculated by adding a fixed number of seconds to the current event loop time.
For example:
1 2 3 |
... # calculate a deadline deadline = current_time + 5 |
This can then be used in the asyncio.timeout_at() context manager to execute one or more asyncio tasks with the deadline.
For example:
1 2 3 4 5 6 7 |
... # set a deadline async with asyncio.timeout_at(deadline): # execute long running task result = await long_running_task() # process result # ... |
If the current time exceeds the provided deadline, the current task that is running is cancelled and a asyncio.TimeoutError exception is raised.
As such, this exception should be handled.
If some other task cancels the running task, an asyncio.CancelledError exception may be raised. Therefore, we may want to handle the asyncio.CancelledError exception.
Finally, if a task itself fails and raises an exception, this will be propagated back to the caller if the result of a task is retrieved. Therefore, any expected exceptions should be handled.
For example, a robust call to asyncio.timeout_at() may look as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
... # get the event loop object loop = asyncio.get_running_loop() # get the current event loop time current_time = loop.time() # calculate a deadline deadline = current_time + 5 # handle exceptions try: # run tasks with a deadline timeout async with asyncio.timeout_at(deadline): # execute long running task result = await long_running_task() # process result # ... except asyncio.TimeoutError: # log timeout, task was canceled by timeout except asyncio.CancelledError: # log some other task canceled it except Exception as ex: # log failure |
Note, replace the “except Exception” with specific exceptions raised in the task. It’s not a great idea to catch the base class Exception.
You can learn more about the asyncio.timeout_at() context manager in the tutorial:
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
How to Set Timeout Delay?
Setting an appropriate timeout delay in asyncio depends on the specific requirements and characteristics of your application.
Below are some heuristics and considerations to help you determine a good timeout value:
- Service Level Agreements (SLAs): Consider any service-level agreements or performance requirements your application needs to meet. Timeout values can be based on these SLAs. For example, if an external API guarantees a response within 2 seconds, you might set a timeout of 2 seconds for requests to that API.
- Observational Testing: Perform observational testing to determine the typical response times of external services or operations. Collect data on how long these operations usually take and use that data to set timeout values.
- Safety Margin: Add a safety margin to observed response times. Setting a timeout slightly higher than the typical response time can help account for occasional delays or variations.
- Task Complexity: Consider the complexity of the task or operation you’re timing out. Complex tasks might require longer timeouts. Conversely, simple operations might need shorter timeouts.
- Network Latency: Take into account network latency. Network operations may require longer timeouts to accommodate variations in network conditions.
If tasks that timeout are retrieved, we might want to consider a new or different timeout, such as a Progressive Backoff.
- Progressive Backoff: In scenarios where you need to retry an operation upon timeout, consider using progressive backoff. Start with a shorter timeout, and if a timeout occurs, progressively increase the timeout for subsequent retries.
What to Do On Timeout
(How to Handle TimeoutError)?
When a timeout occurs in an asyncio application, it’s essential to consider appropriate actions to handle the situation gracefully.
The specific actions to take on a timeout depend on your application’s requirements and the context in which the timeout occurred. Here are some common actions to consider:
- Logging: Log the timeout event to record the occurrence and gather diagnostic information. Include details such as the timestamp, the operation that timed out, and any relevant context.
- Error Handling: Raise an appropriate exception to signal that a timeout occurred. This exception can be caught and handled in your code, allowing you to implement custom error recovery logic.
- Retry: Depending on the nature of the operation and your application’s requirements, you might choose to retry the operation upon timeout. Implement a retry strategy with progressively longer timeouts or a maximum number of retries.
- Fallback or Default Value: If the operation had an expected result and a timeout occurred, consider returning a fallback value or a default result. This can provide a reasonable response to the caller instead of failing the operation entirely.
- Cancellation: If the timed-out operation involves other coroutines or tasks that are no longer needed, you can use asyncio.Task.cancel() to cancel them. This can help free up resources and prevent unnecessary work.
- Resource Cleanup: If the timed-out operation allocated resources that need to be cleaned up, perform the necessary resource cleanup actions to release those resources gracefully.
- Graceful Termination: In some cases, a timeout might signal that the entire application or a specific task needs to terminate gracefully. Implement termination logic to ensure that resources are released and ongoing operations are stopped cleanly.
The specific actions you choose to take on a timeout should align with your application’s design, resilience, and user experience goals.
It’s important to handle timeouts gracefully to ensure that your application remains robust and responsive, even when facing delays or failures in external services or operations.
Further Reading
This section provides additional resources that you may find helpful.
Python Asyncio Books
- Python Asyncio Mastery, Jason Brownlee (my book!)
- Python Asyncio Jump-Start, Jason Brownlee.
- Python Asyncio Interview Questions, Jason Brownlee.
- Asyncio Module API Cheat Sheet
I also recommend the following books:
- Python Concurrency with asyncio, Matthew Fowler, 2022.
- Using Asyncio in Python, Caleb Hattingh, 2020.
- asyncio Recipes, Mohamed Mustapha Tahrioui, 2019.
Guides
APIs
- asyncio — Asynchronous I/O
- Asyncio Coroutines and Tasks
- Asyncio Streams
- Asyncio Subprocesses
- Asyncio Queues
- Asyncio Synchronization Primitives
References
Takeaways
You now know about the importance of timeouts in asyncio and how to add timeouts to your programs.
Did I make a mistake? See a typo?
I’m a simple humble human. Correct me, please!
Do you have any additional tips?
I’d love to hear about them!
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Sergey Lapunin on Unsplash
Do you have any questions?