How to Run pg isready and mysqladmin ping in Health Checks
Check database readiness with pg_isready and mysqladmin ping: exit codes, Docker healthchecks, scripts and monitoring, and what they do not test.

A database that has started is not the same as one that is ready, and scripts that connect too early fail in confusing ways. Two small tools answer the readiness question without credentials or queries. They fit startup scripts, container healthchecks and monitors, as part of the dependency handling in waiting for a service and throwaway databases. The commands assume PostgreSQL 17 and MySQL 8.4 client tools.
pg_isready
pg_isready -h db.example.test -p 5432 -d appdb -U app -t 3
echo "exit: $?"db.example.test:5432 - accepting connections
exit: 0
db.example.test:5432 - rejecting connections exit 1 (starting up, shutting down, recovery)
db.example.test:5432 - no response exit 2 (down or unreachable)
pg_isready: invalid option exit 3 (bad invocation)
The tool opens a connection to the server and does not authenticate, so it needs no password and puts no load on the database. The exit status is documented: 0 ready, 1 rejecting (the server is up but not accepting yet, for example during crash recovery), 2 no response, 3 no attempt made. -t sets the timeout in seconds. It is part of the PostgreSQL client package, so it is available wherever psql is.
mysqladmin ping
mysqladmin --host=db.example.test --port=3306 --user=healthcheck --connect-timeout=3 ping
echo "exit: $?"mysqld is alive
exit: 0
The ping returns exit 0 when the server answers, including when the account is refused (the message says "access denied" yet the server is alive, so exit 0 in some versions), and exit 1 when it cannot be reached. Read the message if you need to distinguish. Use a dedicated low-privilege account, or none, since ping needs no privileges. For MariaDB the tool is the same; for newer MariaDB images it may be called mariadb-admin. For Redis, redis-cli ping prints PONG, see redis-cli.
In scripts: wait with a limit
wait_for_pg() {
local tries=${1:-30}
until pg_isready -q -t 2; do
(( --tries > 0 )) || { echo "postgres not ready" >&2; return 1; }
sleep 1
done
}
wait_for_pg 60 && psql -c 'SELECT 1'The loop waits up to a minute, then fails with a message instead of hanging a deploy. Using the exit status in an until keeps the script short. For MySQL replace the test with mysqladmin ping --silent. After readiness, run a real query to confirm that credentials and permissions work, because readiness says nothing about them.
In Docker and Compose
services:
db:
image: postgres:17
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"]
interval: 5s
timeout: 3s
retries: 10
start_period: 15s
mysql:
image: mysql:8.4
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 10s
timeout: 3s
retries: 6Compose marks the container healthy after the first success, and dependent services with condition: service_healthy wait for it, as in Compose in production. A healthcheck that connects through the container's local socket is not the same as one that connects over the network the application uses; test the path that matters. Keep intervals long enough that the check does not add load.
In monitoring
*/1 * * * * pg_isready -q -h db.example.test -t 5 && curl -fsS -o /dev/null https://hc.example.test/ping/db-up
A cron line like this pings a heartbeat monitor while the database is ready, so downtime triggers an alert, see dead man's switches. A readiness check is only the first layer: also monitor replication lag, disk space, connection counts and slow queries.
What they do not test
They show that the process accepts connections. They do not show that a login works, that a table exists, that replication is healthy or that a query returns in time. For a deeper check, run a query as the application user: psql -c 'SELECT 1' or mysql -e 'SELECT 1'. Pooling proxies such as PgBouncer answer pg_isready even if the database behind them is down. See the pg_isready documentation and the mysqladmin reference.
What readiness means for different consumers
Three audiences ask the question and need different answers. An orchestrator deciding whether to route traffic wants a fast yes or no from a cheap probe. A deploy script deciding whether to run migrations wants to know that the server accepts authenticated connections and that the schema is at the expected version. A human on call wants the reason it is not ready. The tools in this post serve the first audience. They are the right shape for a probe that runs every few seconds, and the wrong shape for the others, which need real queries and messages.
A layered approach works well. The probe checks that the process accepts connections. A startup script, once the probe passes, runs a query as the application user to verify credentials and runs a migration status command. A separate monitoring query checks replication lag and long-running transactions. Each layer runs at its own frequency and fails with its own message, so the failure points at the layer that broke.
Startup behaviour of databases
Servers go through phases at boot, and each phase answers probes differently. PostgreSQL replays its write-ahead log after a crash, and during that time it rejects connections with a message that it is starting up, which pg_isready reports with exit status 1. A large recovery can take minutes, so a timeout of ten seconds in a deploy script fails a database that was about to be ready. Base the wait on the worst recovery you expect, and log the reported state so a slow start is distinguishable from a dead server.
The official container images add a twist. On first start, they run initialisation scripts against a temporary server bound only to the local socket, stop it, and then start the real server. A probe over TCP fails during the first phase, and a probe over the local socket can succeed during it, so a healthcheck that uses the socket may declare the container healthy before the real server is listening. Probe the same address the application will use, and expect one early false positive on first run unless the check requires two consecutive successes.
Authentication, privileges and the probe account
A probe that does not authenticate cannot be broken by a password change, which is a strength. When a deeper check is needed, create a dedicated account with no privileges beyond connecting and the ability to run SELECT 1. Store its password in the secret store like any other, and keep it separate from the application's account so that rotating one does not break the other. Never use the administrator account in a probe: probes run constantly, appear in logs and are copied into monitoring configuration where secrets leak.
Connection limits matter as well. Each probe uses a connection slot briefly, and a check that runs every second from ten places can crowd out real clients on a server with a small limit. Space probes by several seconds and cap the number of sources, or let a pooler absorb them.
Interpreting a failing probe
When the probe fails, work down the stack. Is the host reachable: try ping or a TCP connection with nc -zv host 5432. Is the port open, and is a firewall dropping packets, which looks like a timeout and not a refusal? Is the process running: check systemctl status or docker ps. Does the log mention recovery, a full disk or a configuration error? The exit status of the probe gives the first branch, and the log gives the cause. Recording both in the alert message saves the responder the first five minutes of every incident.


