Skip to content
shellhacks.dev
Databases

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.

6 min read
Summarize in AI
Illustration for "How to Run pg isready and mysqladmin ping in Health Checks": a terminal session, a scrolling log, an exit status badge.

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

Shell
pg_isready -h db.example.test -p 5432 -d appdb -U app -t 3
echo "exit: $?"
Text
db.example.test:5432 - accepting connections
exit: 0
Text
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

Shell
mysqladmin --host=db.example.test --port=3306 --user=healthcheck --connect-timeout=3 ping
echo "exit: $?"
Text
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

Shell
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

YAML
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: 6

Compose 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

Text
*/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.

Also inDevOps
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.