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!

Ready to Learn More Web Development?

Join my Mailing List to receive two articles per week.


I send two weekly emails on building performant and resilient Web Applications with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.

Continue Reading?

Here are a few more Articles for you ...


How to Avoid Conflicts and Let Your OS Select a Random Port

Learn how to have your OS randomly select a port number for your web server to get around the issue of hard-coded ports during development.

By Christoph Schiessl on Python, FastAPI, and DevOps

Disabling 304 Not Modified in FastAPI's StaticFiles

Dealing with caching issues in FastAPI's StaticFiles sub-application and a monkey patching workaround to disable caching.

By Christoph Schiessl on Python and FastAPI

The Built-in id() Function

Learn about object identities and comparisons in Python. Discover the built-in id() function, the is and is not operators, and more.

By Christoph Schiessl on Python

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast web applications.


I'm available for hire as a freelance web developer, so you can take advantage of the more than a decade of experience I have collected working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm also a skilled writer who takes pride in breaking down technical concepts into the simplest possible terms.