Wait For a Service Port With nc in a Loop
Looping nc -z until it succeeds makes a CI step wait for a service container to accept connections before tests run.
Service containers report "started" before they actually accept connections. A short nc -z loop closes that gap and stops flaky "connection refused" failures at the start of a test run.
What it does
The pattern calls nc -z in a bash loop, sleeping between attempts, until the port accepts a connection or a deadline passes. It turns a fixed sleep (which is either too short and flaky or too long and slow) into a condition that polls the real readiness signal.
Common usage
# wait up to 30s for Postgres, then proceed or fail
for i in $(seq 1 30); do
nc -z localhost 5432 && break
echo "waiting for postgres ($i)"; sleep 1
done
nc -z localhost 5432 || { echo "postgres never came up"; exit 1; }Why a fixed sleep is worse
| Approach | Problem |
|---|---|
| sleep 5; run tests | Flaky if the service is slow, wasteful if it is fast |
| nc -z loop | Proceeds the instant the port is ready, fails fast if it never opens |
| Docker healthcheck | Best when available, but not every image ships one |
In CI
A bounded loop (seq 1 N with sleep 1) gives a hard ceiling so a genuinely dead service fails the job instead of hanging until the global timeout. A port being open still does not mean the app finished migrations, so some stacks add an application-level health probe after the nc check.
Common errors in CI
If the loop always falls through to the failure branch, the host is often wrong: inside a GitHub Actions service container the host is localhost, but in a docker-compose network it is the service name, not localhost. "nc: bad address" means DNS could not resolve that service name yet, which is itself a readiness or network issue.