Object storage has a way of being the thing you find out about last. Your backups write to it, your photo library serves from it, your CI stores artifacts in it, your container registry keeps layers in it, your log stack flushes chunks to it. When MinIO degrades, you do not get a MinIO alert; you get a restic error at 3 a.m., a registry that cannot accept pushes, a photo app full of broken thumbnails, and a Loki that quietly stops ingesting. By the time someone connects the dots, the second drive has failed too.

MinIO also fails in a specifically layered way. A node can be up while the cluster has lost write quorum. The API can be healthy while a bucket policy change turned every public asset into a 403. The whole thing can be perfect while the certificate expired, so every client refuses to talk to it. And the nightly mirror to the off-site bucket, the one that makes all of this recoverable, can stop running without producing a single error anywhere. This guide puts one CronAlert monitor on each layer. The HTTP checks and SSL expiry run on the free plan; the keyword assertions and heartbeats need Pro.

Layer 1: the process is up (liveness and readiness)

Every MinIO server exposes unauthenticated health probes on the API port, the same ones Kubernetes uses. The liveness probe is the simplest external check:

URL:              https://s3.example.com/minio/health/live
Method:           GET
Expected status:  200

A 200 means the process on whichever node answered is serving. MinIO returns 429 from this endpoint when the node is shedding load, which with expected status 200 reads as down; that is a reasonable thing to be told about, since a load balancer would be pulling that node at the same moment. /minio/health/ready is the per-node readiness probe and goes 503 when, for example, a configured etcd is unreachable. Behind a load balancer the two are nearly interchangeable for an external monitor; pick live and move on to the check that actually describes the cluster.

MinIO on a bare IP such as http://10.0.0.5:9000 cannot be reached by a public monitor at all, and a bare public IP cannot be reached by HTTP checks from Cloudflare's network either. Give it a hostname, or if it is only on your LAN, rely on the heartbeats in layer 5, which are outbound. A bare public IP can be covered with a TCP monitor on port 9000, which proves the socket accepts connections and nothing more.

Layer 2: the cluster has quorum

This is the check that separates "MinIO" from "a web server". In a distributed deployment with erasure coding, MinIO keeps serving after drives and nodes fail, right up to a threshold, and crossing the write threshold happens before crossing the read threshold. The cluster health endpoint reports exactly that:

URL:              https://s3.example.com/minio/health/cluster
Expected status:  200          (503 = no write quorum)

URL:              https://s3.example.com/minio/health/cluster/read
Expected status:  200          (503 = no read quorum)

Point both at the load-balanced hostname so any surviving node can answer. A red cluster monitor with a green read monitor is the state you most want to be told about: everything still downloads, your users have noticed nothing, uploads and backups are failing, and the next drive failure takes reads. A red read monitor is an outage. Both endpoints are unauthenticated and need nothing but a GET, so they run on the free plan at 3-minute intervals.

What these cannot tell you is that one specific node is offline while quorum holds. For that you want MinIO's Prometheus metrics (minio_cluster_health_nodes_offline_count in the current metrics API) or, if you do not run Prometheus, a liveness monitor per node on each node's own hostname. Four small monitors for a four-node cluster is a fine trade for knowing you are one failure from degraded.

Layer 3: a real object comes back

Health endpoints describe MinIO's opinion of itself. A policy mistake, a reverse proxy that routes /minio/health but not /bucket/, or a bucket that was deleted all leave them green. Read an actual object.

HTTP monitors cannot sign S3 requests (Signature Version 4 is computed per request from your secret key, and a monitor sends fixed headers), so a straightforward read needs an anonymously readable object. Make one tiny canary bucket for the purpose rather than loosening a real one:

mc mb myminio/health-canary
echo "canary-ok-$(hostname)" | mc pipe myminio/health-canary/canary.txt
mc anonymous set download myminio/health-canary
URL:              https://s3.example.com/health-canary/canary.txt
Expected status:  200
Keyword (Pro):    contains "canary-ok"

The download policy allows anonymous GET of objects but not listing, so the bucket root still answers 403 and nobody can enumerate it. If your real public assets are served from a bucket, add a second monitor on one of them, since that is the policy you actually care about; monitoring CDN and static assets covers the cache-in-front case, where a CDN hit can hide an origin that is already failing.

For a store with nothing public, the inverse monitor is the trick. An anonymous GET on the API root should be refused, and the refusal is itself proof that the S3 layer, not just a TCP port, answered:

URL:              https://s3.example.com/
Expected status:  403
Keyword (Pro):    contains "AccessDenied"

A 200 here would mean someone made the server list buckets anonymously, which you also want to hear about. A 502 or a timeout means the proxy or the process is gone. The custom headers guide explains why a non-200 expected status is a legitimate and useful monitor rather than a workaround.

For an authenticated read, use a heartbeat. A one-line cron does a real signed stat and reports only on success:

*/5 * * * * mc stat myminio/backups/.canary >/dev/null \
  && curl -fsS --retry 3 https://cronalert.com/api/heartbeat/<read-token>

Set the heartbeat to a 5-minute interval with a 5-minute grace. Run it from a host that is not a MinIO node, so the check does not die with the thing it checks.

Layer 4: the certificate

MinIO commonly runs with a certificate you manage yourself, in ~/.minio/certs or a mounted secret, outside whatever renews the certificates on your web servers. When it expires, nothing in MinIO reports a problem; every client simply refuses to connect, and the error surfaces in restic, in Velero, in the registry, and in the photo app, all at once and all phrased differently. Enable SSL monitoring on the API hostname (and the console hostname if it is separate). It is on every plan and alerts days ahead. SSL certificate monitoring covers the thresholds, and if you renew with certbot, monitoring the renewal job catches the hook that copies the new cert into MinIO's directory and forgets to restart it.

Layer 5: the jobs that make it recoverable

A bucket is only as safe as the last successful copy of it somewhere else, and copies are made by scheduled jobs that fail by not running. Whether it is mc mirror to an off-site bucket, restic or borgmatic backing up into the bucket, velero snapshots, or a rclone sync, the pattern is the same: ping a heartbeat only on success.

0 2 * * * mc mirror --overwrite --remove myminio/backups offsite/backups \
  && curl -fsS --retry 3 https://cronalert.com/api/heartbeat/<mirror-token>

Create the heartbeat with a 24-hour interval and a grace that covers the longest normal run plus slack, say 3 hours for a mirror that usually takes one. The alert then means: the cron did not fire, the host is down, or mc exited non-zero. That third case is the only one you would otherwise hear about, and only if someone reads the cron mail. Monitoring scheduled backups goes deeper on verifying what was written rather than only the exit code, and batch job monitoring on picking intervals for jobs whose duration varies.

If you use MinIO's built-in bucket replication instead of a cron, there is no job to instrument, so instrument the outcome: a cron on the target side that mc stats an object the source updates daily (a timestamp file written by the backup) and pings a heartbeat when the timestamp is today's. Replication that is silently stuck shows up as a stale file within a day.

Other S3-compatible stores

The quorum endpoints are MinIO's, but the shape of the monitoring carries over.

  • AWS S3. A private bucket's virtual-host URL (https://bucket.s3.amazonaws.com/) answers 403 anonymously, so the inverse monitor applies as-is. A static-website-hosting endpoint answers 200 on its index document. S3 itself is a third-party dependency; what you are monitoring is your bucket, your policy, and your jobs, not Amazon.
  • Cloudflare R2. Public buckets get an r2.dev or custom hostname that serves objects with a 200; monitor one canary object. There is no anonymous API root to inverse-check.
  • Garage, SeaweedFS, Ceph RGW. Each has its own unauthenticated health path (Garage exposes one on its admin API port; check your version's docs for the exact route and whether it is bound publicly). Where none is reachable, the canary object, the inverse root check, and the heartbeat stat work on any S3 implementation, since they only use the S3 API.

Self-hosted apps that use the bucket deserve their own monitors too; the Nextcloud and Immich guide and the NAS guide cover the consumers that most often sit in front of a MinIO.

Reading the alerts

AlertWhat brokeFirst thing to check
Liveness down, everything downProcess, host, or the proxy in frontsystemctl status minio, proxy logs, DNS
Liveness 429Node shedding loadRequest rate, disk latency on that node
Cluster 503, cluster/read 200Lost write quorum: too many drives or servers offlinemc admin info; which drives show offline
Cluster/read 503Lost read quorum; outageSame, urgently
Canary object 403 or 404Bucket policy changed, bucket deleted, or proxy misroutemc anonymous get on the bucket; proxy config
API root returns 200 instead of 403Anonymous listing enabledBucket and IAM policies, now
Signed-read heartbeat overdue, HTTP checks greenCredentials revoked, IAM policy changed, or the checking host is downThe cron host; mc admin user info
Mirror heartbeat overdueBackup did not run or failedCron log on the backup host; free space on the target
SSL expiringCertificate renewal missed, or renewed but MinIO not restartedCert dates in ~/.minio/certs, then restart

Alert channels in CronAlert are team-wide, so every monitor here alerts the same channels. If the storage team wants paging and the app teams want Slack, put the MinIO monitors in their own team; alert routing with teams explains how.

Frequently asked questions

Should I monitor the MinIO Console too?

If it is exposed on its own hostname or port (9001 by default), a plain 200 check on its root catches a crashed console, and SSL monitoring on it catches a separate certificate. It is lower priority than everything above: clients talk to the API port, not the console, and the console has changed shape across MinIO releases.

Can I monitor an individual MinIO node when I only have the load-balanced hostname?

Only if each node also has its own resolvable hostname. If they do, a liveness monitor per node is the cheapest way to learn about one node down while quorum holds. If they do not, the cluster endpoints plus Prometheus metrics are the alternative.

My MinIO is only reachable inside the cluster or the LAN. What then?

Publish the API through a Cloudflare Tunnel if you want inbound checks, or run the signed-read heartbeat and the mirror heartbeat from a host on that network. Both are outbound requests to CronAlert and work from anywhere with egress. For a MinIO on Kubernetes, a CronJob running mc stat and curling the heartbeat does the same thing without exposing anything.

Does a presigned URL work as a monitor target?

For a week at most, since S3 caps presigned expiry at seven days, after which the monitor goes red on its own. The anonymous canary object or the heartbeat script is the durable version of the same idea.

Which plan do I need?

The liveness, quorum, canary, and inverse-root checks are HTTP monitors with an expected status, available on every plan at 3-minute intervals on Free and 1-minute on Pro, and SSL expiry monitoring is free. Keyword assertions and the heartbeats for signed reads and mirror jobs are Pro ($5/mo, or $4/mo billed annually).

Monitor the store before its consumers do

Every service that depends on your object storage will eventually tell you it is broken, in its own words, at the wrong time. Four HTTP monitors and two heartbeats tell you first, and tell you which layer: process, quorum, policy, certificate, or the backup that makes the rest recoverable. Create a free account, add the liveness and cluster monitors, and upgrade to Pro for the mirror heartbeat before the next silent night. Related reading: monitoring Docker and self-hosted apps if MinIO runs in a container, monitoring the server underneath, monitoring Proxmox for the hypervisor beneath that, what each HTTP status code means for monitoring, and choosing between HTTP, TCP, and heartbeat monitors.