How to Avoid Conflicts and Let Your OS Select a Random Port
by Christoph Schiessl on Python, FastAPI, and DevOps
As software engineers, we frequently start various server processes that open up specific ports to listen to incoming requests. In most cases, we want to select a known port number by hand since we want to interact with those server processes manually. For web developers, the most common type of service is HTTP servers. For instance, I usually start my Python web application to listen on port 8000
, then use my browser or an HTTP client like httpie
in the console to feed HTTP requests to it. Sometimes, however, we don't care about port numbers — on the contrary, we don't want to write down the port number because hard-coding would limit us from running multiple instances of the same server in parallel.
To give a concrete example, imagine a web server that's only started to run E2E tests in a web browser. There's no way around starting a real server because the remote-controlled browser is a different process. The two processes need a way to communicate with each other. For web browsers, the networking stack is the way to talk to other processes, specifically HTTP servers. Long story short, a server process with an open port to listen for HTTP requests from the web browser is needed.
This may seem far-fetched, but a hard-coded port number is suboptimal for this use case because it removes the possibility of running multiple instances of the same test suite in parallel. You may not do this often, but testing two feature branches in parallel to check against regressions isn't too hard to imagine.
The solution is to build your server processes to avoid the need for resources that can only ever be acquired by a single process. One example of such a resource is specific port numbers — there can only ever be one, and only one process that can bind to any given port number. Another way to say this is that ports, by definition, cannot be shared between processes.
Playing with the socket
module
Before doing anything else, I want to demonstrate the problem when two processes try to bind to the same port number. For that, we are using the socket
module from Python's standard library.
Firstly, I create a new socket object with the help of the socket()
function. This object represents an IPv4 socket that cannot yet do anything useful because it's not yet bound to any port number.
Secondly, I used the socket object's bind()
method to attempt to bind it to port number 8000
. As you can see, this raised an OSError
because a different process on my system was already bound to the same port number, confirming that ports are exclusive and cannot be shared between processes.
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> sock = socket.socket()
>>> sock.bind(('', 8000))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OSError: [Errno 98] Address already in use
I quickly want to mention that the parameter of the bind()
method depends on the socket's address family, which I didn't specify. Therefore, it uses the default, which is IPv4. Anyway, for IPv4 sockets, the parameter must be of the type tuple[str, int]
. The str
selects the interface, and the int
selects the port number to which to bind.
So, what is an interface? Generally speaking, computers can have an arbitrary number of interfaces with different IP addresses. Most computers have at least two: a loopback interface with IP address 127.0.0.1
, which your computer uses to talk to processes that it is running itself and a "real" interface with actual networking hardware with which your computer talks to other computers.
I used an empty string to select the interface in the example above. This special value tells the socket to bind to the given port number on all of the computer's interfaces. Knowing this, you now also have a complete interpretation of the raised OSError
. Namely, the error really means that port 8000
was already bound to a different socket for at least one of my computer's interfaces.
Letting your OS select a Random Port
So, how can we get around this? How can you tell in advance if a given port number is already in use? You don't have to because you can tell your operating system, which must be aware of all bound ports, to randomly select a port number for you. To demonstrate this, we can again use Python's low-level socket
module:
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> sock = socket.socket()
>>> sock.bind(('', 0))
>>> sock.getsockname()
('0.0.0.0', 45913)
As before, I'm creating a socket object, but I bind to port 0
instead of port 8000
. The range of possible ports goes from 0
to 2 ** 16 - 1 == 65535
, but some port numbers have special meaning. Port number 0
is one of these special numbers that makes your operating system randomly select a free port number for you.
Once the socket is successfully bound to a port number, we can use the getsockname()
method to determine which port the OS has selected. The documentation explains this method as follows:
Return the socket’s own address. This is useful for finding the port number of an IPv4/v6 socket, for instance. (The format of the address returned depends on the address family—see above.)
As you can see above, the function returns a value of type tuple[str, int]
because the socket uses the IPv4 address family. In this case, it tells us that the socket is bound to 0.0.0.0
(i.e., all interfaces) and port 45913
.
Starting FastAPI apps with a Random Port
The cool thing about this is that it's pretty low-level functionality, which is usually also available through high-level APIs. For instance, if you use uvicorn
to serve your FastAPI application, you can ask it to bind to port 0
. This is then passed through to the underlying socket implementation of your OS and ultimately results in the selection of a random port ...
import uvicorn
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
app = FastAPI()
@app.get("/hello", response_class=PlainTextResponse)
async def hello():
return "Hello World"
if __name__ == "__main__":
# uvicorn uses port 8000 by default, but we're running it
# with port 0 to trigger the random port selection of the OS.
uvicorn.run(app=app, port=0)
$ python app.py
INFO: Started server process [977021]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:41507 (Press CTRL+C to quit)
INFO: 127.0.0.1:41507 - "GET /hello HTTP/1.1" 200 OK
$ http GET http://127.0.0.1:41507/hello
HTTP/1.1 200 OK
content-length: 10
content-type: text/plain; charset=utf-8
date: Sun, 28 Apr 2024 14:10:00 GMT
server: uvicorn
Hello World
In this case, my OS selected port number 41507
, but the next time the application starts, it will randomly select a different one. You can also run the same application multiple times in parallel, and each process will automatically pick a different port number.
That's everything for today. Thank you for reading, and see you soon!