Address Already in Use in Go and How to Free the Port
Fix listen tcp bind address already in use in a Go server: find the process holding the port, handle containers and restarts, and use port 0 in tests.

You start the server and it exits at once with:
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
listen tcp :8080: bind: address already in useThe Go program did nothing wrong. The listen call asked the operating system for port 8080, and the system said that some socket already owns it. The message is written by the kernel's bind call and wrapped by the net package, so every language prints a version of it, and the fix is the same everywhere: find the owner, then decide whether to stop it or move your server.
This article covers the quick fix, the causes behind it from most to least common, the Go-specific behavior worth knowing, and how to stop it happening in tests and deployments. It belongs to the web cluster that ends in a production HTTP server, and it is one entry in the larger catalogue of server error messages. Everything ran on Go 1.25.6 on macOS.
The quick fix
Ask the system who holds the port. On macOS and most Linux systems:
lsof -i :8080On Linux, ss -ltnp 'sport = :8080' shows the process and PID, and on Windows netstat -ano | findstr :8080 gives the PID to look up in Task Manager. You will see a command name and a PID. If it is an old copy of your own program, stop it with kill PID. If it is something else that legitimately uses the port, give your server a different one. That resolves nine out of ten cases, and the rest of this article is about the tenth, and about not meeting the first nine again.
Where the other owner comes from
A previous run that never exited. The most common cause by far: you started the server in a terminal tab you forgot, or a go run in a background job, or an editor's debug session that is paused in the debugger with the socket still open. go run adds a wrinkle: it builds a binary and runs it as a child, so killing the go run parent with some signals can leave the child alive. Look for your binary name in lsof.
A file watcher that restarts too fast. Tools such as air or reflex stop the old process and start the new one. If the old process takes a moment to shut down (because it waits for in-flight requests, as it should; see graceful shutdown), the new one tries to bind before the port is free and fails. Raise the watcher's delay, or make shutdown faster in development.
Docker and port mappings. A container started with -p 8080:8080 holds host port 8080 for as long as it runs, even if the process inside is idle, and a stopped container that was never removed with --rm can still be restarting under a policy. docker ps lists published ports. Two compose services mapping the same host port conflict the same way. Note that a conflict can be between a container and a program you run on the host, which is why "it works in Docker" and "it fails on my laptop" sometimes happen at once.
A system service or another project. macOS AirPlay Receiver has been known to use port 5000 and 7000, a local database admin tool may sit on 8080, and a second checkout of the same project is running on the default port. lsof names them.
IPv4 and IPv6, wildcard and specific addresses. Binding :8080 listens on all interfaces; binding 127.0.0.1:8080 listens only on loopback. Whether the two can coexist depends on the operating system, so a conflict that appears on one machine can be absent on another. The program below checks one case.
What Go does and does not do
The net package calls bind with the address you gave, after setting SO_REUSEADDR on the socket for listeners on Unix systems. That option matters for one famous worry: after a server restarts, the old connections linger in the TIME_WAIT state for a minute or two, and without SO_REUSEADDR a new bind to the same port would fail during that time. Go sets the option, so a quick restart does not run into TIME_WAIT. If you see this error right after a restart, the port is really still owned by a process, not by lingering connections.
The next program reproduces the error and its neighbours: a second listener, port 0, a wildcard-then-specific bind, a privileged port and SO_REUSEPORT.
package main
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"syscall"
)
var portRe = regexp.MustCompile(`:\d{4,5}`)
func mask(s string) string { return portRe.ReplaceAllString(s, ":PORT") }
func main() {
// 1. The error itself: two listeners, one address.
first, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
defer first.Close()
addr := first.Addr().String()
_, err = net.Listen("tcp", addr)
fmt.Println("same address twice: ", mask(err.Error()))
// 2. Port 0 means "pick any free port": nothing can collide.
a, _ := net.Listen("tcp", "127.0.0.1:0")
b, _ := net.Listen("tcp", "127.0.0.1:0")
fmt.Println("two listeners on port 0: ", a.Addr().String() != b.Addr().String(), "(different ports)")
a.Close()
b.Close()
// 3. The wildcard against a specific address on the same port.
wild, _ := net.Listen("tcp", ":0")
port := wild.Addr().(*net.TCPAddr).Port
_, err = net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
fmt.Println("wildcard, then loopback: ", mask(fmt.Sprint(err)))
wild.Close()
// 4. A port below 1024 as an ordinary user (skipped when run as root).
_, err = net.Listen("tcp", "127.0.0.1:80")
fmt.Println("port 80 as a normal user: ", mask(fmt.Sprint(err)))
// 5. SO_REUSEPORT lets several listeners share a port; the kernel spreads connections between them.
lc := net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error {
var serr error
if err := c.Control(func(fd uintptr) { serr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1) }); err != nil {
return err
}
return serr
}}
r1, err := lc.Listen(nil, "tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
defer r1.Close()
r2, err := lc.Listen(nil, "tcp", r1.Addr().String())
fmt.Println("second listener with SO_REUSEPORT:", err == nil)
if err == nil {
defer r2.Close()
}
_, err = net.Listen("tcp", r1.Addr().String()) // an ordinary listener still cannot join them
fmt.Println("ordinary listener on that port: ", mask(fmt.Sprint(err)))
// 6. In tests, let the test server choose the port.
srv := httptest.NewServer(http.NotFoundHandler())
fmt.Println("httptest picked a port: ", strings.HasPrefix(srv.URL, "http://127.0.0.1:"))
srv.Close()
}same address twice: listen tcp 127.0.0.1:PORT: bind: address already in use
two listeners on port 0: true (different ports)
wildcard, then loopback: <nil>
port 80 as a normal user: listen tcp 127.0.0.1:80: bind: permission denied
second listener with SO_REUSEPORT: true
ordinary listener on that port: listen tcp 127.0.0.1:PORT: bind: address already in use
httptest picked a port: trueThe first line is the error. The second shows the way out for any program that does not need a fixed port: ask for port 0 and read the assigned port from Addr(). The third line is the portability warning: on this macOS machine, binding the wildcard address and then a specific address on the same port succeeded. I did not test Linux or Windows, and their rules for overlapping binds differ, so check the platform you deploy on rather than assuming. The fourth is a different failure with a similar look: bind: permission denied for a port below 1024 as an ordinary user, which is fixed by using a higher port behind a proxy, or by granting the binary the capability to bind low ports (setcap 'cap_net_bind_service=+ep' on Linux) instead of running as root.
The last block is SO_REUSEPORT, set through a net.ListenConfig control function. With it, two listeners could bind the same port, and the kernel spreads incoming connections between them; the plain net.Listen that follows still got address already in use, because every socket on the port must opt in. That is a legitimate technique for running several accept loops or doing zero-downtime handover between old and new processes. It is also a trap on a development machine: with the option set, a forgotten second copy of your server no longer produces an error, and half your requests quietly go to the old code. Use it deliberately, and not as a way to silence this error.
Fixing it for good
In tests, never use a fixed port. Use httptest.NewServer, which listens on 127.0.0.1:0, or listen on port 0 yourself and pass the address to the code under test. A test suite with a hard-coded port collides with itself when packages run in parallel, which go test ./... does by default, and with the developer's own running server. The last line of the output shows the helper doing its job; see also testing without a server.
Make the port configurable and fail clearly. Read the address from configuration, as in the configuration article, and when Listen fails, log the address you tried. A line such as cannot listen on :8080: bind: address already in use (is another copy running?) saves a minute for every person who meets it.
Fail fast, or retry briefly. For a service restarted by an orchestrator, exit with a non-zero status and let the supervisor try again. During a rolling deploy where the old process is still draining on the same port, a bounded retry (a few attempts over a few seconds) is reasonable. Do not loop forever; a permanent conflict should surface.
Hand ports over, do not fight for them. To restart a service without refusing connections, use socket activation (systemd passes the listening socket to the new process), SO_REUSEPORT with a controlled handover, or put a load balancer in front and start the new instance on another port first. Each of these removes the window in which neither process owns the port.
Close what you open. In long-running programs that create listeners on demand (a test helper, a dev proxy), defer ln.Close() and stop servers with Shutdown; a listener leaked by a test can hold a port until the test binary exits, and the next test then fails with this very error.
A last check
If lsof shows nothing and the error persists, look at the wording: permission denied, cannot assign requested address (the IP is not on this machine, a common mistake with a hard-coded address from another environment) and address already in use have different causes. The same call fails with the last text on Windows as Only one usage of each socket address (protocol/network address/port) is normally permitted. The behavior of the underlying options is documented in the net package, and the operating system side is in the Linux bind(2) manual page.


