Expand description
holdon
Wait for anything. Know why if it doesn't.
A next-gen "wait for service ready" CLI in Rust. One static binary, parallel by default, protocol-aware, with diagnostic failures that actually tell you what broke.
$ holdon postgres://db:5432 redis://cache:6379 https://api/health
✓ ready postgres://db:5432 · 27ms
✓ ready redis://cache:6379 · 14ms
✗ failed https://api/health · 5.0s · ▁▂▄▆█ · 510ms
├ dns ✓ 2ms
├ tcp ✓ 3ms
└ http ✗ status 503
hint: service may still be initializing
→ 2/3 ready · 5.1s§Why holdon
Diagnostic stages, not “timed out”. Every probe is multi-stage (DNS, TCP, TLS, protocol). When a target fails you get the stage that broke and an operator-facing hint, not a stack trace.
Parallel by default. Pass a dozen targets in one command. They run concurrently. Sequential mode is opt-in via --sequential.
Protocol-aware probes for 15 schemes. TCP, HTTP, DNS, file, exec, log, Postgres, MySQL/MariaDB, Redis, MongoDB, RabbitMQ (AMQP), Kafka, Temporal, InfluxDB (v1/v2/v3), and gRPC Health/Check. Each probe speaks the real protocol instead of just opening a socket.
Type-safe URL DSL. mongodb://, kafka://, temporal://, etc. Query parameters validated at parse time. URL passwords and ?token= values redacted in every error path, in Display, in Debug, and in CLI parse errors.
One static binary. musl build is under 4 MB with default features, under 1.5 MB with no defaults. No runtime, no shell-out, no OpenSSL anywhere in the dependency tree.
Rustls everywhere. Postgres, MySQL, Redis, MongoDB, RabbitMQ, Kafka, Temporal, HTTP, and gRPC all share one TLS stack with bundled webpki roots. No native-tls.
Machine output. --output json emits a stable line-delimited schema (v: 1) ready for jq. POSIX-aligned exit codes (0, 2, 124, 126, 127, 130, 143).
§Install
The recommended path is cargo:
cargo install holdonPick a feature set based on which probes you need:
cargo install holdon --no-default-features --features http,postgres
cargo install holdon --features all-databases
cargo install holdon --features fullSkip the compile step with cargo binstall:
cargo binstall holdonHomebrew (macOS, Linux):
brew install imjustprism/holdon/holdonScoop (Windows):
scoop bucket add holdon https://github.com/imjustprism/scoop-holdon
scoop install holdonPrebuilt binaries for Linux (gnu/musl, x86_64 + aarch64), macOS (x86_64 + arm64), and Windows ship with every release:
curl -fsSL https://raw.githubusercontent.com/imjustprism/holdon/main/install.sh | shOr grab a tarball from GitHub Releases.
A multi-arch Docker image is published to the GitHub Container Registry:
docker pull ghcr.io/imjustprism/holdon
docker run --rm ghcr.io/imjustprism/holdon tcp://db:5432Verify the install:
holdon --versionMinimum supported Rust version: 1.85.
§Quickstart
holdon :5432 # wait for localhost:5432
holdon :5432 :6379 :3000 # several ports in parallel
holdon :5432 -- npm run migrate # exec a command once ready
holdon https://api.local/health -t 60s # http with custom timeout
holdon postgres://user:pw@db/app # postgres handshake
holdon exec:///usr/local/bin/check.sh # custom readiness commandThe argument after -- is the command to run once every target is ready. holdon execs it directly (no shell), so quoting and signals work the same as timeout(1) or kubectl exec.
§Protocols
| Scheme | What it checks |
|---|---|
tcp://, :port, host:port | DNS resolve, TCP connect |
http://, https:// | TCP, TLS, HTTP request (-H, --method, --expect-body, --expect-body-regex, --expect-json, --no-follow-redirects, --ca-cert, --tls-min) |
dns:// | Hostname resolves |
file:///path | Path exists (?mode=absent inverse) |
postgres://, postgresql:// | Connect + SELECT 1 (TLS by default) |
mysql://, mariadb:// | Connect + SELECT 1 (TLS by default) |
redis://, rediss:// | Connect + PING (rediss:// for TLS) |
grpc://, grpcs:// | grpc.health.v1.Health/Check unary (optional /Service path) |
influxdb://, influxdbs:// | /ping for v1, v2, v3. Optional ?expect-version=1|2|3 and ?token=... (Bearer/Token auth for v3 OSS) |
mongodb://, mongodb+srv:// | Connect + admin ping command (SRV-aware) |
amqp://, amqps:// | RabbitMQ AMQP connect, optional ?queue= / ?exchange= passive declare |
kafka://, kafkas:// | Kafka broker Metadata fetch, optional ?topic= and ?expect-partitions= |
temporal://, temporals:// | Temporal server gRPC Health/Check on WorkflowService |
log:///path?match=... | Wait for a substring or regex to appear in a local log file (last 1 MiB) |
exec://program?arg=... | External command, ready iff exit 0 |
§Feature flags
Defaults (http + json-output) cover most CI use cases. Database and message-broker probes are opt-in to keep the default binary small.
| Feature | Adds |
|---|---|
http | HTTP / HTTPS probes (rustls) |
postgres | Postgres probe via tokio-postgres + rustls |
mysql | MySQL / MariaDB probe via mysql_async + rustls |
redis | Redis probe via redis crate + rustls |
mongodb | MongoDB probe via mongodb driver + rustls (SRV-aware) |
rabbitmq | RabbitMQ AMQP probe via lapin + rustls (optional queue/exchange check) |
kafka | Kafka Metadata probe via pure-Rust rskafka + rustls (optional topic/partition check) |
temporal | Temporal server gRPC Health/Check probe (depends on grpc) |
influxdb | InfluxDB /ping probe (depends on http) |
grpc | gRPC Health/Check probe via tonic + rustls |
json-output | --output json line-delimited events |
all-databases | postgres + mysql + redis + mongodb |
full | Everything above |
§Config file
Pass --config holdon.toml, or drop holdon.toml / .holdon.toml next to where you run holdon and it’s auto-detected.
interval = "200ms"
timeout = "60s"
success_threshold = 2
targets = [
"tcp://db:5432",
"https://api.local/health",
]Explicit CLI flags always win over the config file. See examples/holdon.toml.
§Output modes
- Plain (default). Live spinner, colored status, sparklines on stderr. Auto-disabled in non-TTY environments and when
NO_COLORis set. - JSON (
--output json). Line-delimited events on stdout, stable schema documented indocs/json-schema.md. Versioned (v: 1). Adding fields is non-breaking, removing or renaming is. - Quiet (
-q). Only the exit code.
§Exit codes
| Code | Meaning |
|---|---|
0 | All targets ready |
2 | CLI misuse or parse error |
124 | Overall timeout elapsed (GNU timeout convention) |
126 | Exec’d child not executable |
127 | Exec’d child binary not found |
130 | Interrupted by SIGINT (Ctrl-C) |
143 | Interrupted by SIGTERM |
Override the timeout exit code with --timeout-exit-code <N> when wrapping in Docker/Kubernetes lifecycle hooks that expect a specific code.
§Shell completions and man page
holdon --generate-completion bash > /etc/bash_completion.d/holdon
holdon --generate-completion zsh > ~/.zsh/completions/_holdon
holdon --generate-completion fish > ~/.config/fish/completions/holdon.fish
holdon --generate-completion power-shell | iex
holdon --generate-manpage > /usr/local/share/man/man1/holdon.1Prebuilt completions for every shell plus the man page are attached to each release as holdon-completions-and-manpage.tar.gz.
§Library
holdon is also a Rust crate. The same probe engine is exposed through Runner and Target:
use std::time::Duration;
use holdon::{Runner, Target};
use holdon::runner::RunnerConfig;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let targets = vec![
"postgres-host:5432".parse::<Target>()?,
"redis-host:6379".parse::<Target>()?,
];
let cfg = RunnerConfig::default().timeout(Duration::from_secs(30));
let report = Runner::new(cfg).run(targets, None).await;
report.assert_all_ready()?;
Ok(())
}See the examples directory and the API docs.
§Security
- TLS is rustls only. No OpenSSL anywhere in the tree.
cargo-denyblocks it. - Rustls everywhere. Every TLS-capable probe (HTTP, Postgres,
MySQL, Redis,MongoDB,RabbitMQ, Kafka, Temporal, gRPC) uses the same ring-backed rustls stack with bundled webpki roots. - Password redaction. URL passwords are stripped in
Display,Debug, and every error path. Same for?token=query values on schemes that accept them. - Parse errors scrub secrets. CLI errors like “invalid target …” percent-decode query keys before matching, so
?to%6Bken=...cannot bypass the redaction. - HTTP redirect policy. Followed up to 5 hops.
https → httpdowngrades refused. --insecureis HTTP-only. Prints a stderr warning on every run. Do not use in production.exec://runs whatever you point it at. Treat target strings as code at the invocation site.file://andlog://usesymlink_metadata. Symlinks are not followed into attacker-controlled paths.- No telemetry. No phone-home, no analytics, ever.
See SECURITY.md for the full threat model and disclosure instructions.
§Contributing
Bug reports, feature requests, and PRs are welcome.
- Branch naming:
feat/<short-name>,fix/<short-name>,docs/<short-name>,chore/<short-name>. - Run
cargo fmt,cargo clippy --all-targets --all-features -- -D warnings, andcargo test --all-featuresbefore opening a PR. - New probes follow the
src/checker/<name>.rsshape: apub(super) async fn probe(...)returningVec<Stage>plus a feature gate inCargo.toml.
§Star History
§Contributors
§License
Dual MIT or Apache-2.0, at your option.
Public Rust API for the holdon wait-for-readiness tool.
The CLI binary is the primary surface. This library exposes the same probe
engine for programmatic use. Most callers want the Runner and Target
pair: build a config, parse targets, drive the runner, inspect the
resulting Report.
Re-exports§
pub use error::Error;pub use error::Result;pub use runner::Direction;pub use runner::Event;pub use runner::Report;pub use runner::Runner;pub use runner::RunnerConfig;pub use runner::Schedule;pub use runner::TargetReport;pub use target::Hostname;pub use target::LogMatcher;pub use target::Target;pub use util::parse_duration;