tcpwait

package module
v2.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 15 Imported by: 0

README

tcp-wait

╭──────────────────────────────────────╮
│   🐴 DonkeyX's tcp-wait              │
╰──────────────────────────────────────╯

        //\\
       (/oo\)   .--------.
       (____)  | WAIT... |
        /||\   '--------'
       //||\\   📡 ping / ready / blip
      ^^ ^^ ^^
   "Is postgres back yet?"

Small tool to wait on stuff being up before you start the real process. Started life as a container pre-start helper; now it’s also an importable Go package, and has a watch mode for those upgrade / firewall blips.

Works as:

  • CLItcp-wait in Docker / k8s / scripts
  • Libraryimport "github.com/donkeyx/tcp-wait/v2"

| dockerhub | https://hub.docker.com/r/donkeyx/tcp-wait | | ghcr | ghcr.io/donkeyx/tcp-wait | | github | https://github.com/donkeyx/tcp-wait | | docs | https://pkg.go.dev/github.com/donkeyx/tcp-wait/v2 |

Same donkey stable as cluster-utils-api / cluster-utils — this one’s the “wait until the other thing is actually up” bit.

What it can wait on

Kind Ready when
tcp port accepts a connection
http GET returns 200 (or whatever status you ask for)
redis PING gets PONG (not just “port is open”)
postgres answers a startup message (skips “starting up” / recovery noise)
file path exists (marker file, socket path, etc.)

TCP alone lies sometimes — redis can still be LOADING, postgres can accept sockets while it’s not ready. The redis/postgres checks speak just enough protocol that you don’t need go-redis / pgx hanging off this thing.

License is MIT — use it however you like; keep the copyright notice.

Install

This is v2 (library + probes + watch). Module path has the /v2 suffix — that’s normal Go for majors.

# docker
docker run --rm donkeyx/tcp-wait:latest -version
docker run --rm ghcr.io/donkeyx/tcp-wait:latest -version

# go (note: cmd path — root is the library now)
go install github.com/donkeyx/tcp-wait/v2/cmd/tcp-wait@latest

# or grab a release binary
# https://github.com/donkeyx/tcp-wait/releases

Library:

go get github.com/donkeyx/tcp-wait/v2@latest

v1 was CLI-only at the module root (go install github.com/donkeyx/tcp-wait@v1.0.2 still works if you need the old binary).

CLI

# basic tcp (-hp is the old flag, still works; -tcp is the same thing)
tcp-wait -hp github.com:443
tcp-wait -tcp db:5432,cache:6379 -t 60

# http readiness
tcp-wait -http http://api:8080/readyz -t 30

# expected status and repeatable request headers
tcp-wait -http https://api:8443/readyz -http-status 204 \
  -http-header 'Authorization: Bearer token' -t 30

# TLS TCP/Redis probes; certificate verification remains enabled by default
tcp-wait -tls -tcp db:5432 -redis cache:6379 -t 60

# actually talk redis / postgres
tcp-wait -redis cache:6379 -postgres db:5432 -t 60

# wait for a file someone else writes
tcp-wait -file /var/run/app.ready -t 30

# wait, then exec your app (handy as a container entrypoint)
tcp-wait -tcp db:5432 -http http://api:8080/readyz \
  -t 60 -dial 2s -interval 500ms \
  -- /app/server --config /config.yaml
Watch mode (the upgrade / firewall thing)

Normal mode exits as soon as everything is green. Watch mode keeps poking and only logs changes — up, down, or a blip (was up, now down). Super useful when you’re bouncing postgres/redis or waiting for a security group to actually open.

# sit on it until Ctrl-C (-t 0 = no time limit)
tcp-wait -watch -postgres db:5432 -redis cache:6379 -t 0 -interval 500ms -o text

# or cap the window (e.g. firewall change)
tcp-wait -w -tcp $HOST:5432 -t 600 -interval 1s -o text

You’ll see something like:

level=INFO msg="check up" check=postgres://db:5432
level=WARN msg="check blip" check=postgres://db:5432 err="..."
level=INFO msg="check up" check=postgres://db:5432
level=INFO msg="watch stopped" status="all up" reason=signal

check blip = it was good, then it wasn’t. That’s the hole during the upgrade.

Flags
Flag Default Notes
-hp / -tcp host:port,... TCP
-http URLs, GET, expect 200
-http-status 200 expected status for every -http URL
-http-header repeatable Name: value request header
-redis host:port,... PING
-postgres host:port,... startup probe
-file paths that must exist
-t 20 overall timeout seconds; 0 = no limit
-dial 1s per-attempt timeout
-interval 1s sleep between attempts
-o json json or text
-q off errors only (skip this for watch — you’ll hide the interesting lines)
-w / -watch off continuous up/down/blip logging
-tls off TLS for TCP/Redis; configure HTTPS TLS settings
-tls-server-name TLS certificate server-name override
-tls-insecure-skip-verify off disable TLS certificate verification; development only
-version version + git hash
-- cmd… after wait succeeds, exec the command (don’t mix with -watch)

Logs: -o json (default, fine for k8s/loki) or -o text (nicer in a terminal). Final wait results include status (ready, timeout, or error) and checks fields for machine consumers.

Exits:

  • wait: 0 ready, 1 timeout / bad flags, 130 if you Ctrl-C
  • watch: 0 on Ctrl-C, or on timeout if everything’s up; 1 if the timer runs out still degraded
k8s initContainer
initContainers:
  - name: wait-deps
    image: donkeyx/tcp-wait:latest
    args: ["-postgres", "db:5432", "-redis", "cache:6379", "-t", "120", "-o", "text"]
compose / entrypoint wrap
command: ["tcp-wait", "-tcp", "db:5432", "-t", "60", "--", "/app/server"]

Library

package main

import (
	"context"
	"errors"
	"log"
	"time"

	tcpwait "github.com/donkeyx/tcp-wait/v2"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	err := tcpwait.Wait(ctx, []tcpwait.Check{
		tcpwait.TCP("db:5432"),
		tcpwait.Redis("cache:6379"),
		tcpwait.Postgres("db:5432"),
		tcpwait.HTTP("http://api:8080/readyz"),
		tcpwait.File("/var/run/app.ready"),
	})
	if err != nil {
		// errors.Is(err, tcpwait.ErrTimeout) — message lists who never came up
		log.Fatal(err)
	}

	// knobs if you need them
	_ = tcpwait.WaitWithOptions(ctx, []tcpwait.Check{tcpwait.TCP("db:5432")}, tcpwait.Options{
		DialTimeout: time.Second,
		Interval:    500 * time.Millisecond,
	})

	// watch until ctx is done (timeout / cancel)
	res, _ := tcpwait.WatchWithOptions(ctx, []tcpwait.Check{
		tcpwait.Postgres("db:5432"),
		tcpwait.Redis("cache:6379"),
	}, tcpwait.Options{Interval: 500 * time.Millisecond})
	if !res.AllUp() {
		log.Printf("still down: %v", res.Down())
	}
}

TCP-only shortcut: tcpwait.WaitTCP(ctx, []string{"db:5432", "redis:6379"}).

Docs on pkg.go.dev (go doc . works locally either way).

Develop

Needs Go 1.26+.

go test -race ./...
go build -o bin/tcp-wait ./cmd/tcp-wait

make test
make build
make docker-build   # local image tcp-wait:local
CI / release (same shape as cluster-utils-api)
Workflow When What
CI PR / master test, build, smoke, goreleaser check
Docker PR / master PR = amd64 build only; push = multi-arch + GHA cache → Hub/GHCR
Release v* tag GoReleaser binaries + multi-arch images + Hub README sync
CodeQL PR / master / weekly scan

Docker Hub auth matches cluster-utils-api (no USER/PASS leftovers):

Name Type Environments
DOCKERHUB_USERNAME variable ci (docker workflow) + deployment (release)
DOCKERHUB_TOKEN secret same — Hub access token (Read/Write)

Same values as the API repo is fine. Without DOCKERHUB_USERNAME set, Hub login is skipped (GHCR still works).

Hub image push can work while README sync returns 403 — token needs description rights, or ignore it; images still publish.

goreleaser check
make snapshot   # needs docker + goreleaser

git tag v2.0.1
git push origin v2.0.1

License

MIT — see LICENSE.

Documentation

Overview

Package tcpwait waits until network endpoints or paths are ready.

Overview

Use Wait / WaitWithOptions to block until every Check succeeds (typical container pre-start / initContainer). Use Watch / WatchWithOptions to keep probing and log up/down/blip transitions (upgrade windows, firewall opens) until the context is cancelled.

Supported probe kinds (prefer constructors over filling Check by hand):

  • TCP — host:port accepts a connection
  • HTTP / HTTPStatus / HTTPWithHeaders — GET returns an expected status (default 200)
  • Redis — RESP PING → PONG
  • Postgres — StartupMessage answered (auth or non-startup error)
  • File — filesystem path exists

CLI

A command-line tool is provided at github.com/donkeyx/tcp-wait/v2/cmd/tcp-wait:

go install github.com/donkeyx/tcp-wait/v2/cmd/tcp-wait@latest

Errors

Wait returns an error wrapping ErrTimeout when the context deadline fires before all checks pass. Use errors.Is(err, tcpwait.ErrTimeout). The message lists checks that never succeeded.

Watch returns ctx.Err() when stopped (context.Canceled or DeadlineExceeded) and a WatchResult with the last known state of each check.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrTimeout = errors.New("services did not respond")

ErrTimeout is returned when the context deadline is hit before all checks pass. Use errors.Is(err, ErrTimeout). The error string lists checks that never succeeded.

Functions

func Wait

func Wait(ctx context.Context, checks []Check) error

Wait runs all checks in parallel until each succeeds or ctx is done. Defaults: 1s dial timeout, 1s retry interval, no logging.

Example

ExampleWait shows building checks and waiting until they pass (or time out).

package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	tcpwait "github.com/donkeyx/tcp-wait/v2"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
	defer cancel()

	err := tcpwait.Wait(ctx, []tcpwait.Check{
		// tcpwait.TCP("db:5432"),
		// tcpwait.Redis("cache:6379"),
		// tcpwait.Postgres("db:5432"),
		// tcpwait.HTTP("http://api:8080/readyz"),
		// tcpwait.File("/var/run/app.ready"),
		tcpwait.TCP("127.0.0.1:1"), // nothing listening — will time out in this example
	})
	if errors.Is(err, tcpwait.ErrTimeout) {
		fmt.Println("timed out")
		return
	}
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("ready")
}
Output:
timed out

func WaitTCP

func WaitTCP(ctx context.Context, addrs []string) error

WaitTCP waits until each host:port accepts a TCP connection. Convenience wrapper around Wait + TCPAddrs.

Example

ExampleWaitTCP is a shortcut when you only need TCP accept checks.

package main

import (
	"context"
	"fmt"
	"time"

	tcpwait "github.com/donkeyx/tcp-wait/v2"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
	defer cancel()

	if err := tcpwait.WaitTCP(ctx, []string{"127.0.0.1:1"}); err != nil {
		fmt.Println("not ready")
		return
	}
	fmt.Println("ready")
}
Output:
not ready

func WaitTCPWithOptions

func WaitTCPWithOptions(ctx context.Context, addrs []string, opts Options) error

WaitTCPWithOptions is WaitTCP with options.

func WaitWithOptions

func WaitWithOptions(ctx context.Context, checks []Check, opts Options) error

WaitWithOptions is like Wait with explicit dial/retry/logging options.

Types

type Check

type Check struct {
	Kind   Kind
	Target string // host:port for tcp/redis/postgres; URL for http; path for file

	// HTTP: expected status code (default 200).
	ExpectStatus int
	// HTTP request headers.
	Headers map[string]string

	// Postgres: StartupMessage user/database (defaults: postgres/postgres).
	Username string
	Database string
}

Check is a single readiness probe. Prefer constructors (TCP, HTTP, Redis, Postgres, File) over filling the struct by hand.

func File

func File(path string) Check

File waits until path exists (regular file, socket, directory, etc.). Useful for readiness marker files or unix socket paths.

func HTTP

func HTTP(url string) Check

HTTP waits until GET url returns ExpectStatus (default 200).

func HTTPStatus

func HTTPStatus(url string, status int) Check

HTTPStatus is like HTTP but with an explicit expected status code.

func HTTPStatusWithHeaders added in v2.1.0

func HTTPStatusWithHeaders(url string, status int, headers map[string]string) Check

HTTPStatusWithHeaders is like HTTPStatus with request headers.

func HTTPWithHeaders added in v2.1.0

func HTTPWithHeaders(url string, headers map[string]string) Check

HTTPWithHeaders waits until GET url returns 200 with the supplied headers.

func Postgres

func Postgres(addr string, userDB ...string) Check

Postgres waits until PostgreSQL at addr answers a StartupMessage (auth request or a non-"starting up" error). Optional user/database default to "postgres".

func Redis

func Redis(addr string) Check

Redis waits until a Redis PING receives PONG at addr (host:port).

func TCP

func TCP(addr string) Check

TCP waits until addr (host:port) accepts a TCP connection.

func TCPAddrs

func TCPAddrs(addrs ...string) []Check

TCPAddrs is a convenience: one TCP check per host:port.

func (Check) String

func (c Check) String() string

String returns a stable id for logs and timeout errors (e.g. "tcp://db:5432").

type Kind

type Kind string

Kind identifies how a Check probes readiness.

const (
	KindTCP      Kind = "tcp"
	KindHTTP     Kind = "http"
	KindRedis    Kind = "redis"
	KindPostgres Kind = "postgres"
	KindFile     Kind = "file"
)

type Options

type Options struct {
	// DialTimeout is how long each probe attempt may take (dial / HTTP client / etc).
	// Default: 1s.
	DialTimeout time.Duration
	// Interval is how long to wait between failed attempts for a check. Default: 1s.
	Interval time.Duration
	// Logger receives per-attempt warnings and per-check ready messages.
	// Nil discards logs.
	Logger *slog.Logger
	// TLS enables TLS for TCP and Redis probes, and configures HTTPS probes.
	TLS bool
	// TLSServerName overrides the server name used for certificate verification.
	TLSServerName string
	// TLSInsecureSkipVerify disables certificate verification. Use only for
	// trusted development or private test endpoints.
	TLSInsecureSkipVerify bool
}

Options configure Wait. Zero values use defaults.

type WatchResult

type WatchResult struct {
	// Up maps check label (Check.String()) → last successful probe.
	Up map[string]bool
}

WatchResult is the last known state of each check when Watch stops.

func Watch

func Watch(ctx context.Context, checks []Check) (WatchResult, error)

Watch repeatedly probes all checks until ctx is cancelled. It logs only state transitions:

INFO  "check up"   — first success, or recovery after down
WARN  "check down" — first failure while never up yet
WARN  "check blip" — transition from up → down (the upgrade hole)

Defaults match Wait (1s dial, 1s interval, quiet logger). The returned error is usually ctx.Err() (Canceled / DeadlineExceeded).

Example

ExampleWatch shows continuous monitoring (upgrade / firewall blips). Cancel the context (timeout or signal) to stop; inspect WatchResult.

package main

import (
	"context"
	"fmt"
	"time"

	tcpwait "github.com/donkeyx/tcp-wait/v2"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 400*time.Millisecond)
	defer cancel()

	res, err := tcpwait.WatchWithOptions(ctx, []tcpwait.Check{
		tcpwait.TCP("127.0.0.1:1"),
	}, tcpwait.Options{
		Interval:    100 * time.Millisecond,
		DialTimeout: 50 * time.Millisecond,
	})
	_ = err // typically context.DeadlineExceeded
	if res.AllUp() {
		fmt.Println("ended all up")
	} else {
		fmt.Println("ended degraded")
	}
}
Output:
ended degraded

func WatchWithOptions

func WatchWithOptions(ctx context.Context, checks []Check, opts Options) (WatchResult, error)

WatchWithOptions is Watch with explicit options.

func (WatchResult) AllUp

func (r WatchResult) AllUp() bool

AllUp reports whether every check was up on the last probe.

func (WatchResult) Down

func (r WatchResult) Down() []string

Down returns labels that were down on the last probe (sorted).

Directories

Path Synopsis
cmd
tcp-wait command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL