go / freeport

I use ephemeral port binding to find an available port for integration tests that spawn a subprocess and need to hand it the port via env var. For in-process HTTP tests, prefer httptest.NewServer.

The pattern

import (
	"fmt"
	"net"
	"testing"
)

func pickFreePort(t testing.TB) string {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen :0: %v", err)
	}
	port := fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port)
	_ = ln.Close()
	return port
}

Binding to port 0 asks the OS for any available ephemeral port. The listener is closed immediately so the subprocess can bind it.

Usage

When a test spawns a separate process (via exec.CommandContext) that does its own listening:

func TestEndToEnd(t *testing.T) {
	port := pickFreePort(t)
	srvURL := "http://127.0.0.1:" + port

	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	t.Cleanup(cancel)

	cmd := exec.CommandContext(ctx, "go", "run", "./cmd/myserver")
	cmd.Env = []string{"PORT=" + port}
	if err := cmd.Start(); err != nil {
		t.Fatalf("start: %v", err)
	}
	t.Cleanup(func() { _ = cmd.Process.Kill() })

	// Poll until ready
	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		if resp, err := http.Get(srvURL + "/health"); err == nil {
			resp.Body.Close()
			break
		}
		time.Sleep(100 * time.Millisecond)
	}

	// ... actual test ...
}

For in-process HTTP tests, use httptest

If the server runs in the same process as the test, skip this helper. httptest.NewServer allocates a port, starts the server, and hands you a URL and a client:

srv := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(srv.Close)

resp, err := srv.Client().Get(srv.URL + "/path")

The free-port pattern is only for the case where something else does the binding.

Caveats

There's a TOCTOU (time-of-check vs. time-of-use) window between ln.Close() and the subprocess binding to the port: another process could grab it in between. On a developer or CI machine this is rare, and a readiness poll catches the failure mode quickly. For production servers, configure ports explicitly instead.

When to use

When not to use

← All articles