You can run the asyncio server as a background task in an asyncio program.
This has the benefit of allowing the main() coroutine to perform other tasks while the server is running and to cancel the server on demand if needed.
The asyncio server will not close correctly if it is canceled. We can close the asyncio server safely manually with a try-except or try-finally block. Alternatively, we can automatically close the asyncio server when it is canceled by using the context manager interface on the server object itself.
In this tutorial, you will discover how to run the asyncio server as a background task and safely close the server on demand.
Let’s get started.
Need to Run Asyncio Server As a Background Task
The typical way to use the asyncio server is to create an instance of the server object and run it until the user forcefully terminates the program.
For example:
1 2 3 4 5 |
... # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', port=8888) # accept client connections forever (kill via control-c) await server.serve_forever() |
You can learn more about starting an asyncio server in the tutorial:
The problem with this approach is that it does not allow any control over the server because the main coroutine is suspended while the server accepts client connections.
How can we run an asyncio server in the background and perform other activities in our program while the server is running?
Run loops using all CPUs, download your FREE book to learn how.
How to Run An Asyncio Server As a Background Task
An asyncio server can be run as a background task.
This can be achieved by defining a coroutine that creates the server and accepts client connections forever. The coroutine can then be scheduled as an asyncio task and run in the background.
In treating the server itself as a task, it allows other benefits too, such as:
- Ability to perform other tasks while the server is running or after the server has stopped.
- Ability to cancel the server task and close the server on request.
- Ability to await the server task if the main coroutine chooses.
This can be achieved by first defining a coroutine to create and run the server.
For example:
1 2 3 4 5 6 |
# task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', port=8888) # accept client connections forever (kill via control-c) await server.serve_forever() |
This coroutine can then be scheduled as a task from the main coroutine.
For example:
1 2 3 |
... # start and run the server as a background task server_task = asyncio.create_task(background_server()) |
The main coroutine can then choose to await it if it chooses.
For example:
1 2 3 |
... # wait for the server await server_task |
It can also choose to cancel the server task.
For example:
1 2 3 |
... # cancel the server task server_task.cancel() |
A limitation of this approach is that if the server task is canceled, it will terminate immediately, and will not shut down safely.
How to Shutdown The Asyncio Server Task Safely
We can shut down the asyncio server task safely by wrapping the body of the task in a try-finally block.
The finally block can call the close() method on the server and wait for all client connections to be closed before moving on
If the server fails with an unexpected exception or is canceled, the finally block will execute and safely close the server.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', port=8888) try: # accept client connections forever (kill via control-c) await server.serve_forever() except asyncio.CancelledError: # stop taking new clients server.close() # wait for all clients to close await server.wait_closed() # re-raise cancellation raise |
Thankfully, we don’t have to implement this ourselves.
The asyncio.Server class implements the context manager interface and will close the server and all client connections for us.
Therefore, we can achieve the same thing with less code as follows:
1 2 3 4 5 6 7 8 |
# task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', port=8888) # ensure the server is closed correctly async with server: # accept client connections forever (kill via control-c) await server.serve_forever() |
This will ensure that regardless of how the server is terminated, it is closed safely.
Now that we know how to run the asyncio server as a background task and how to close it safely, let’s look at some worked examples.
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.
Example of Creating an Asyncio Server (Foreground Task)
Before we explore how to run an asyncio server as a background task, let’s review how to start and run the server as a foreground task.
In this example, we will define a client handler that does nothing, as we won’t be accepting any client connections. We will then create a new server on port 8888, report its details, and accept client connections forever in the main coroutine via the server_forever() method.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# SuperFastPython.com # example of creating a server and serving forever import asyncio # handler for client connections async def handler(reader, writer): pass # main coroutine async def main(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) # accept client connections forever (kill via control-c) await server.serve_forever() # start the asyncio event loop asyncio.run(main()) |
Running the example starts the asyncio event loop and runs the main() coroutine.
The main() coroutine runs and creates a new server for the local host, accepting connections on port 8888. The server starts accepting client connections as soon as it is created.
The details of the server are then reported, confirming the host address and port.
Finally, the server_forever() method is called and the main coroutine blocks forever while the server accepts client connections.
The server must then be killed by killing the process, such as sending the SIGINT signal via the Control-C keyboard command.
1 |
<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)> |
The problem with this approach to running the server is that the main coroutine is suspended for the lifetime of the server.
It is unable to perform other activities while the server is running.
Next, let’s explore an example of running the asyncio server as a background task.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Example of Asyncio Server in Background Task
We can explore how to run the asyncio server as a background task.
In this case, we can define a new coroutine that creates the server and serves forever.
1 2 3 4 5 6 7 8 |
# task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) # accept client connections forever (kill via control-c) await server.serve_forever() |
We can then schedule this coroutine as a background task, freeing the main coroutine to continue on with other activities.
1 2 3 |
... # start and run the server as a background task server_task = asyncio.create_task(background_server()) |
Recall that when the main coroutine exits, the event loop will cancel all other running coroutines.
This means we can exit the main coroutine normally and the server will be canceled automatically.
You can learn more about the main coroutine in the tutorial:
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# SuperFastPython.com # example of asyncio server in a background task import asyncio # handler for client connections async def handler(reader, writer): pass # task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) # accept client connections forever (kill via control-c) await server.serve_forever() # main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(2) # report progress print('Shutting down...') # start the asyncio event loop asyncio.run(main()) |
Running the example starts the asyncio event loop and runs the main() coroutine.
The main() coroutine runs and creates and schedules the background_server() coroutine as a background task. It then suspends and sleeps for 2 seconds.
The background_server() task runs, creates the server, reports the details of the server, and serves forever accepting client connections.
The main() coroutine resumes and reports a final message before exiting.
The event loop then terminates the background_server() task, forcefully closing the server.
1 2 |
<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)> Shutting down... |
The problem with this approach is that the server task is canceled, and is not allowed to close the connections to any connected clients gracefully.
Next, let’s explore how we can safely close the server when the program exits.
Example of Closing Asyncio Server Safely
We can explore how to safely close the asyncio server when the server task is canceled.
In this case, we can update the background_server() to use a try-except block when accepting client connections forever and handle a request to cancel.
Within the except block, we can report progress, close the server, and wait for all clients to disconnect safely before finally re-raising the asyncio.CancelledError as expected by the caller.
The updated background_server() coroutine with this change is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) try: # accept client connections forever (kill via control-c) await server.serve_forever() except asyncio.CancelledError: # report progress print('Closing server...') # stop taking new clients server.close() # wait for all clients to close await server.wait_closed() # re-raise cancellation raise |
The main() coroutine can then explicitly cancel the server task and wait for the server to shut down.
This can be achieved using the “cancel and wait” idiom.
1 2 3 4 5 6 7 8 |
... # cancel the server task server_task.cancel() # wait for the server to shutdown try: await server_task except asyncio.CancelledError: pass |
You can learn more about cancel and wait in the tutorial:
Tying this together, the updated main() coroutine is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(2) # report progress print('Shutting down...') # cancel the server task server_task.cancel() # wait for the server to shutdown try: await server_task except asyncio.CancelledError: pass |
This allows the main() coroutine to control the life-cycle of the server task and perform any additional cleanup action after the server has shut down.
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# SuperFastPython.com # example of asyncio server in a background task import asyncio # handler for client connections async def handler(reader, writer): pass # task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) try: # accept client connections forever (kill via control-c) await server.serve_forever() except asyncio.CancelledError: # report progress print('Closing server...') # stop taking new clients server.close() # wait for all clients to close await server.wait_closed() # re-raise cancellation raise # main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(2) # report progress print('Shutting down...') # cancel the server task server_task.cancel() # wait for the server to shutdown try: await server_task except asyncio.CancelledError: pass # start the asyncio event loop asyncio.run(main()) |
Running the example starts the asyncio event loop and runs the main() coroutine.
The main() coroutine runs and creates and schedules the background_server() coroutine as a background task. It then suspends and sleeps for 2 seconds.
The background_server() task runs, creates the server, reports the details of the server, and serves forever accepting client connections.
The main() coroutine resumes and reports a final message. It then cancels the server task and suspends, awaiting the task to be done.
An asyncio.CancelledError exception is raised in the background_server() task, which is handled. Progress is reported and the server is closed. It then waits for all client connections to be closed safely before re-raising the asyncio.CancelledError.
The main() coroutine resumes and ignores the asyncio.CancelledError, and terminates the event loop.
This highlights how the main coroutine can explicitly cancel the server when the server is run as a background task.
1 2 3 |
<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)> Shutting down... Closing server... |
A limitation of this approach is that we have to explicitly handle the exception raised in the background_server() and manually shut down the server.
Next, let’s look at how we can automatically close the server safely using the context manager interface.
Example of Closing Asyncio Server Safely With Context Manager
We can explore how to automatically close the server safely using the context manager interface on the asyncio server object itself.
In this case, we can update the background_server() coroutine to use the context manager interface when accepting client connections forever.
For example:
1 2 3 4 5 6 7 8 9 10 |
# task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) # ensure the server is closed correctly async with server: # accept client connections forever (kill via control-c) await server.serve_forever() |
This will have the same effect as the previous example of closing the server on exception and waiting for all clients to disconnect safely.
It has the benefit that it will work for any exception raised in the coroutine, such as an error or cancellation, and uses less code.
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# SuperFastPython.com # example of asyncio server in a background task with context manager import asyncio # handler for client connections async def handler(reader, writer): pass # task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) # ensure the server is closed correctly async with server: # accept client connections forever (kill via control-c) await server.serve_forever() # main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(2) # report progress print('Shutting down...') # cancel the server task server_task.cancel() # wait for the server to shutdown try: await server_task except asyncio.CancelledError: pass # start the asyncio event loop asyncio.run(main()) |
Running the example starts the asyncio event loop and runs the main() coroutine.
The main() coroutine runs and creates and schedules the background_server() coroutine as a background task. It then suspends and sleeps for 2 seconds.
The background_server() task runs, creates the server, reports the details of the server, and opens the context manager then serves forever accepting client connections.
The main() coroutine resumes and reports a final message. It then cancels the server task and suspends, awaiting the task to be done.
An asyncio.CancelledError exception is raised in the background_server() task. The exception is propagated up the stack and the context manager is exited, which automatically closes the server and waits for all client connections to disconnect.
The main() coroutine resumes and ignores the asyncio.CancelledError, and terminates the event loop.
This highlights how we can automatically shut down the server in a safe manner if the server task is canceled or any other exception is raised in the task.
1 2 |
<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)> Shutting down... |
We can confirm that the server is no longer serving after the context manager is exited.
That is, we can confirm that the context manager closes the server after it is exited.
This can be achieved by wrapping the context manager in a try-finally block and reporting the serving status of the server in the finally block.
For example:
1 2 3 4 5 6 7 8 |
... try: # ensure the server is closed correctly async with server: # accept client connections forever (kill via control-c) await server.serve_forever() finally: print(f'Server closed: serving={server.is_serving()}') |
The updated background_server() coroutine with this change is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) try: # ensure the server is closed correctly async with server: # accept client connections forever (kill via control-c) await server.serve_forever() finally: print(f'Server closed: serving={server.is_serving()}') |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# SuperFastPython.com # example of asyncio server in a background task with context manager import asyncio # handler for client connections async def handler(reader, writer): pass # task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) try: # ensure the server is closed correctly async with server: # accept client connections forever (kill via control-c) await server.serve_forever() finally: print(f'Server closed: serving={server.is_serving()}') # main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(2) # report progress print('Shutting down...') # cancel the server task server_task.cancel() # wait for the server to shutdown try: await server_task except asyncio.CancelledError: pass # start the asyncio event loop asyncio.run(main()) |
Running the example starts the asyncio event loop and runs the main() coroutine.
The main() coroutine runs and creates and schedules the background_server() coroutine as a background task. It then suspends and sleeps for 2 seconds.
The background_server() task runs, creates the server, reports the details of the server, and opens the context manager then serves forever accepting client connections.
The main() coroutine resumes and reports a final message. It then cancels the server task and suspends, awaiting the task to be done.
An asyncio.CancelledError exception is raised in the background_server() task. The exception is propagated up the stack and the context manager is exited, which automatically closes the server and waits for all client connections to disconnect. The finally block executes and reports the status of the server, confirming that it is no longer serving.
The main() coroutine resumes and ignores the asyncio.CancelledError, and terminates the event loop.
This highlights that the context manager does indeed close the server as described in the API documentation.
1 2 3 |
<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)> Shutting down... Server closed: serving=False |
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 how to run the asyncio server as a background task and safely close the server on demand.
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.
Do you have any questions?