Starting and Stopping uvicorn
in the Background
by Christoph Schiessl on Python
I recently wrote an article showing how to let your OS automatically pick a random free port for your webservers. This was useful by itself, but today, I want to build on top of it and demonstrate how to start and stop uvicorn
in the background. There are many use cases for this, such as running a test suite that requires a live server, or the reason I came up with this topic: automating a web browser to take screenshots of a series of pages.
Anyway, it doesn't matter why you want to start uvicorn
in the background because the approach I'm about to demonstrate is always the same, regardless of the use case.
So, when you usually start uvicorn
, you use the run()
function, which blocks the current thread. If you directly start the script below with a command such as python app.py
, then the blocked thread happens to be the main thread, which means your program can't do anything else until the webserver terminates.
import uvicorn
from fastapi import FastAPI
app = FastAPI()
uvicorn.run(app=app, port=8000) # blocking call
I don't want a hard-coded port number for the webserver running in the background, so I'm telling it to use port 0
, which causes my operating system to pick a random free port automatically. I'm still using the blocking run()
function, but at least now I can run multiple instances of the same program in parallel because they all use different ports.
import uvicorn
from fastapi import FastAPI
app = FastAPI()
uvicorn.run(app=app, port=0) # blocking call
Behind the scenes, the run()
function instantiates an object of the Server
class and calls its run()
method. When creating a Server
object, we can provide a Config
object with all the details, such as the ASGI application to start and the port number to bind. For the sake of this article, it doesn't matter which ASGI application we are using, so I'm using an empty FastAPI application as a placeholder.
import uvicorn
from fastapi import FastAPI
app = FastAPI()
config = uvicorn.Config(app=app, port=0)
server = uvicorn.Server(config=config)
server.run() # blocking call
To avoid blocking the main thread, we must use a different thread that uvicorn
can block until it terminates. To do so, I import the threading
module and then create an instance of the Thread
class that I initialize with a target
parameter. This parameter must be callable (e.g., a function
object) and provides the code that the new thread executes when it is started with its start()
method.
import threading
import time
import uvicorn
from fastapi import FastAPI
app = FastAPI()
config = uvicorn.Config(app=app, port=0)
server = uvicorn.Server(config=config)
thread = threading.Thread(target=server.run)
thread.start() # non-blocking call
while not server.started:
time.sleep(0.001)
print(f"HTTP server is now running on http://???:???")
Once the new thread is alive, we must wait for the webserver to become ready — just because the thread is alive doesn't mean the server is ready to receive incoming requests. To implement this waiting mechanism, I use a loop to repeatably sleep()
for a small duration of time until the server.started
attribute becomes True
, whose state is maintained by uvicorn
and updated to be True
as soon as the server's startup process is complete.
We have a slight problem at this point because there's no way to determine which port the OS has randomly selected. Hence, if we don't know the port, we can't interact with the server because we don't know where to send our requests. If we had access to the underlying socket, we could ask the socket
object for the port to which it is bound, but we don't because uvicorn
doesn't expose this socket.
The solution is simple: We bring our own socket(s).
Instead of leaving socket management to uvicorn
, it's possible to provide our own socket(s), and this is precisely what I'm doing here. I manually create a socket
object and bind()
it to port 0
for automatic port selection. Then, I put my socket into a single-element list and provide that as a keyword parameter to the run()
method that the new Thread
executes.
import threading
import time
import socket
import uvicorn
from fastapi import FastAPI
app = FastAPI()
config = uvicorn.Config(app=app)
server = uvicorn.Server(config=config)
(sock := socket.socket()).bind(("127.0.0.1", 0))
thread = threading.Thread(target=server.run, kwargs={"sockets": [sock]})
thread.start() # non-blocking call
while not server.started:
time.sleep(0.001)
address, port = sock.getsockname()
print(f"HTTP server is now running on http://{address}:{port}")
When the server is ready, I still have access to the socket object I created myself and can use the getsockname()
method to ask it for the address and port to which it is bound. I can then use this address and port to interact with the server.
Last but not least, we can put all of this in a context manager to ensure that the server is not only starting up but also correctly stopping again once it is no longer needed.
In this case, I create a subclass of uvicorn.Server
and implement a new method, run_in_thread()
. This method is the context manager, and therefore, it returns a Generator
that must yield
exactly one value. Inside run_in_thread()
, I'm managing the thread and waiting for the webserver to be ready. Finally, when the context manager is no longer needed, I ask the server object to exit by setting its should_exit
attribute to True
and joining the thread to make the main thread wait until the uvicorn
thread terminates.
import contextlib
import threading
import time
import uvicorn
from fastapi import FastAPI
from typing import Generator
app = FastAPI()
class Server(uvicorn.Server):
@contextlib.contextmanager
def run_in_thread(self) -> Generator:
thread = threading.Thread(target=self.run)
thread.start()
try:
while not self.started:
time.sleep(0.001)
yield
finally:
self.should_exit = True
thread.join()
config = uvicorn.Config(app=app)
server = Server(config=config)
with server.run_in_thread():
address, port = server.config.bind_socket().getsockname()
print(f"HTTP server is now running on http://{address}:{port}")
So, here you have it. The custom Server
class provides a generic way to run uvicorn
and, thereby, any ASGI application in the background so that it does not block the main thread. Your program can do something else, such as running a test suite while the web server runs in the background. Thank you very much for reading! I hope you found this interesting and to see you again soon!