Pixel Pete, the CanaryHub canaryCanaryHub

Redis health check: from redis-cli ping to a readiness endpoint that pages you

Redis health check: from redis-cli ping to a readiness endpoint that pages you

Quick answer

A Redis health check confirms your app can actually reach Redis, not just that the process is running. In Docker, redis-cli ping returns PONG when the server can serve data. In your app, add a bounded Redis PING to a readiness endpoint that returns HTTP 503 when Redis is unreachable, then point an external monitor at that endpoint so you get paged when it fails. A local container healthcheck only restarts Redis, it never tells you.

A Redis health check should answer one question: can my app actually talk to Redis right now? Not "is the redis process running" (it can be running and refusing connections), and not "did it answer an hour ago." The fastest check is redis-cli ping, which returns PONG when the server is able to serve data. But a ping you run by hand, or a healthcheck that only lives inside your Docker host, tells you nothing when Redis falls over at 3am and you're asleep.

So there are really three layers here, and most guides only cover the first: the container-level healthcheck, the app-level readiness check, and the external monitor that pages a human. Let's build all three.

What does a Redis health check actually check?

PING is Redis's built-in liveness probe. Send it with no argument and a healthy server replies PONG:

redis> PING
PONG

That reply means more than "the socket is open." Redis returns an error to PING when it can't serve data, for example while it's still loading a large dataset from disk on startup, or when you're pointed at a stale replica that has lost sync. So a successful PONG is a genuine signal that Redis is ready to do work, not just that a process exists.

What PING does not tell you is whether your application can reach Redis. Your app talks to Redis over the network, through a connection pool, with credentials, possibly across a private network boundary. Redis can be perfectly healthy while your app can't reach it because of a wrong hostname, an expired password, a firewall rule, or an exhausted pool. That's the difference between a liveness check (is Redis itself alive) and a readiness check (can this app serve traffic that depends on Redis). You want both, in different places.

How do I add a health check to a Redis container in Docker Compose?

If Redis runs as a container, give it a healthcheck so Compose knows when it's actually ready, and so dependent services wait for it instead of crash-looping on startup:

services:
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s

  api:
    build: .
    depends_on:
      redis:
        condition: service_healthy

The four knobs are worth understanding, because the defaults are rarely what you want:

  • interval is how often the check runs. 10s is a reasonable cadence for a dependency.
  • timeout is how long a single check may take before it counts as a failure. Keep it short (a few seconds); a PING that takes 3 seconds is already a problem.
  • retries is how many consecutive failures flip the container to unhealthy. This is your damping against a single flaky check.
  • start_period is a grace window at boot during which failures don't count, so a slow-loading Redis doesn't get marked unhealthy before it's finished warming up.

The depends_on block with condition: service_healthy is the part that saves you real pain: your api service won't start until Redis reports healthy, which kills the classic "app boots, Redis isn't ready, app dies" race.

If Redis requires a password (requirepass), an unauthenticated ping fails with a NOAUTH error, so pass the credential and match the reply explicitly:

    healthcheck:
      test: ["CMD-SHELL", "redis-cli -a \"$REDIS_PASSWORD\" ping | grep -q PONG"]

Keep that password in an environment variable rather than hard-coding it. You can watch the results locally with docker inspect --format '{{json .State.Health}}' <container>.

How do I check Redis from my app's readiness endpoint?

The container healthcheck proves Redis is up. It says nothing about whether your app can reach it. For that, add a Redis check to a readiness endpoint your app already exposes (call it /readyz to keep it distinct from a shallow liveness /healthz).

Two rules make this check safe. First, bound it with a timeout so a hung Redis can't hang your health endpoint and take the whole app down with it. Second, return machine-parseable JSON with a top-level status and a per-dependency checks array, plus a real HTTP status code, so any monitor can read it without scraping HTML.

Here it is in Node with ioredis and Express:

// GET /readyz — deep readiness: can THIS app reach Redis right now?
app.get('/readyz', async (req, res) => {
  const checks = []
  try {
    // race the PING against a 500ms timeout so a stuck Redis can't wedge us
    const pong = await Promise.race([
      redis.ping(),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('redis timeout')), 500)),
    ])
    checks.push({ name: 'redis', status: pong === 'PONG' ? 'ok' : 'down' })
  } catch (err) {
    checks.push({ name: 'redis', status: 'down' })
  }

  const ok = checks.every((c) => c.status === 'ok')
  res.status(ok ? 200 : 503).json({ status: ok ? 'ok' : 'down', checks })
})

The same shape in Python, with redis-py and a socket timeout doing the bounding for you:

import redis

r = redis.Redis(host="localhost", port=6379, socket_timeout=0.5)

@app.get("/readyz")
def readyz():
    checks = []
    try:
        r.ping()  # raises redis.RedisError on failure or timeout
        checks.append({"name": "redis", "status": "ok"})
    except redis.RedisError:
        checks.append({"name": "redis", "status": "down"})

    ok = all(c["status"] == "ok" for c in checks)
    return ({"status": "ok" if ok else "down", "checks": checks},
            200 if ok else 503)

A healthy response looks like this, and it's small enough that any uptime monitor can parse it in a millisecond:

HTTP/1.1 200 OK
Content-Type: application/json

{"status":"ok","checks":[{"name":"redis","status":"ok"}]}

When Redis is unreachable, the same endpoint returns 503 with "status":"down" and the redis check marked down. That is the signal an external monitor keys off of.

Should Redis being down fail the whole health check?

Not always, and this is the nuance most examples skip. Ask whether Redis is a hard dependency:

  • Hard dependency (session store, rate limiter, primary cache with no fallback): if Redis is down your app can't correctly serve requests, so return 503. Traffic and monitors should both treat you as down.
  • Soft dependency (best-effort cache you can miss and fall back to the database): don't take the whole app down because a cache blinked. Return 200 with "status":"degraded" and a checks entry showing redis is down.

A degraded response lets you keep serving while still raising a flag. If your monitor understands a degraded state, it can warn you without paging you out of bed. CanaryHub's probe, for instance, reads that {status, checks} payload directly: a down status or a non-2xx code is an outage, a degraded status surfaces the specific failing check by name so the alert tells you what broke, not just that something did.

Why doesn't a passing health check mean Redis is monitored?

Here's the trap. A Docker healthcheck runs inside your infrastructure. When it fails, Docker (or your orchestrator) restarts the container. That's useful, but it is not monitoring, because nobody outside the box is watching, and nobody gets told. If the whole host goes down, or Redis enters a restart loop, or the network partitions, the local healthcheck is either gone or lying, and you find out from your users.

A health endpoint is inert until something external polls it. The endpoint you built in the last section returns a clean 503 when Redis is unreachable, but a 503 that nobody reads is just a log line. The missing piece is an external monitor that hits /readyz on a schedule, from outside your network, and pages you the moment it sees the failure.

That's the whole job: point an uptime monitor at your readiness URL, tell it to expect a 2xx, and route the alert somewhere you'll actually see it. With CanaryHub that's a Telegram (or Slack, or email) alert the moment /readyz starts returning 503, with the failing redis check named in the message. The check you already wrote does the detecting; the external monitor does the telling. You need both.

How do I catch a Redis-backed worker that died?

Redis is almost never just a cache. It's the backbone of your background jobs: Sidekiq stores all of its job and operational data in Redis, and Celery, BullMQ, and RQ all lean on it the same way. Which creates a blind spot that a /readyz check will never catch.

Your web process pings Redis, gets PONG, and reports healthy. Meanwhile the worker process, the thing that actually drains the queue, has crashed. Redis is up. The web app is up. /readyz is green. And jobs are silently piling up because nothing is processing them. Your health check is telling the truth and still missing the incident, because it's checking the wrong process.

The failure mode here is absence, not error, and you can't catch absence by polling an endpoint. You catch it with a heartbeat, also called a dead man's switch: the worker reports in on every run, and you alert when a report is late. Add one line to the end of the worker's recurring task, after it has actually done its work:

# at the end of the worker's scheduled run, after the queue is drained
curl -fsS -m 10 --retry 3 https://ingest.canaryhub.ai/ping/YOUR_TOKEN

Configure the monitor with the interval you expect (say, a ping every 5 minutes) plus a grace period. As long as the pings keep landing, it stays quiet. The moment one is late, it assumes the worker died and alerts you. Now a green /readyz and a dead Sidekiq can't hide from you at the same time.

The whole picture

A complete Redis health check is three layers, each covering the previous one's blind spot:

  1. A container healthcheck (redis-cli ping) so your orchestrator knows Redis is ready and dependents wait for it.
  2. An app readiness endpoint with a bounded Redis PING returning {status, checks} JSON, so any monitor can tell whether your app can reach Redis.
  3. An external monitor on that endpoint, plus a heartbeat on every Redis-backed worker, so a human actually gets paged, for both "Redis is down" and "the worker that uses Redis is down."

Get all three in place and the 3am Redis outage becomes a Telegram message you can act on, instead of a support ticket you read over breakfast. If cron jobs and background workers are where your risk really lives, the same heartbeat pattern is worth a closer look in our guide to monitoring a cron job with a dead man's switch.

Frequently asked questions

Does redis-cli ping need the password if Redis requires auth?
Yes. If you set requirepass, an unauthenticated PING comes back with a NOAUTH error, which your check should read as unhealthy. Pass the credential in the check: redis-cli -a $REDIS_PASSWORD ping, and expect PONG. Keep the password in an environment variable, not inline in the compose file, so it doesn't leak into logs or your git history.
Should a Redis outage return HTTP 503 or a degraded status?
It depends on whether Redis is a hard dependency. If your app cannot serve requests without it (Redis is your session store or primary cache with no fallback), return 503 so traffic and monitors both treat the app as down. If Redis is only a best-effort cache and you can fall back to the database, return HTTP 200 with a degraded status and a checks entry showing redis is down. You stay up, but you still get a warning that something needs attention.
My /health is green but my Sidekiq jobs stopped. Why?
Because your web process and your worker process are different things. A web /health check pings Redis and passes, but that says nothing about whether the Sidekiq (or Celery, or BullMQ) worker is alive and draining the queue. The worker can crash while Redis and the web app stay perfectly healthy, and jobs pile up in silence. The fix is a heartbeat: have the worker ping a monitoring URL each run, and alert on the absence of that ping.
How often should a Redis health check run?
For a Docker container healthcheck, every 10 to 30 seconds with a short timeout is plenty. For an external monitor hitting your readiness endpoint, every 30 to 60 seconds catches real outages quickly without hammering Redis. Keep the check itself cheap, a single PING, so the monitoring never becomes the load.