Cloudflare Tunnel has become the default way to publish a self-hosted service without opening a port: cloudflared runs next to the service, dials out to Cloudflare's edge, and a public hostname routes through that connection. It's secure and it's convenient, and it moves the point of failure somewhere nobody is watching. The connector is a daemon on a box in a closet. When it stops, the hostname doesn't disappear; it keeps answering, with a Cloudflare error page, and the service behind it is simply gone.
The good news is that the failure is unusually legible from outside. Cloudflare returns different status codes depending on whether the tunnel or the thing behind it is broken, so a plain HTTP monitor can tell you not just that something failed but which half. This guide covers reading those codes, the trap that makes a monitor on an Access-protected hostname useless, a heartbeat from the host for tunnels with no public hostname, and how to read the two together.
Two codes, two failures
Create an HTTP monitor on each public hostname your tunnel serves, expecting 200, exactly as you would for any site; it's on the free plan and includes certificate tracking, though a tunnel hostname's certificate is Cloudflare's and effectively never expires. What makes the tunnel case different is what the failures look like:
- 530 with Cloudflare error 1033 ("Argo Tunnel error" on the error page) means the edge has no connected
cloudflaredfor this tunnel. The host is off, the daemon crashed or was stopped, an outbound firewall rule now blocks it, or its credentials were revoked. Everything on Cloudflare's side is fine. - 502 Bad Gateway means the tunnel is connected and
cloudflaredtried to reach the local service you configured (http://localhost:8096, say) and it refused or didn't answer. The connector is fine; the container or process behind it is down, or the ingress rule points at the wrong port. - A timeout is rarer and usually means the origin accepted the connection but hung; with Cloudflare in the path it typically surfaces as a 524 instead.
- 200 from the wrong thing is the case covered in the next section.
Give the monitor a name that includes the service, and when it fires, the status code in the alert is your first diagnostic step done. The status codes guide covers the rest of the 5xx range Cloudflare can emit.
The Access trap: a monitor that is always green
If the hostname sits behind Cloudflare Access (and the admin panels most people tunnel should), an unauthenticated request gets a redirect to the Access login page. CronAlert follows redirects, as browsers do, and the login page returns 200. So a plain monitor on an Access-protected hostname reports up forever, through a dead tunnel, a dead service, and anything else, because the one thing it's actually checking is Cloudflare's login screen.
The fix is an Access service token. In Zero Trust, open Access → Service Auth → Service Tokens, create one named "CronAlert", and copy its client ID and secret. Add a policy on the application that allows that service token (a "Service Auth" policy action), then add two custom headers to the monitor: CF-Access-Client-Id and CF-Access-Client-Secret. Custom headers are on every plan. Access now passes the monitor's request through to the service, and the monitor finally measures what you meant it to. On Pro, add a keyword that only appears on the application's own page, so a future policy change that sends the monitor back to the login screen is caught rather than silently passing again.
The same trap applies to any login redirect, not only Access: a service whose root redirects to a sign-in page returns 200 from the sign-in page. Point the monitor at an endpoint that answers meaningfully without a session, or use a keyword. Monitoring internal tools works through the general case.
A heartbeat from the host
Some tunnels have no public hostname to check: they carry private-network routes for WARP clients, or SSH, or a database. And even for public ones, a signal from inside tells you whether the connector died or merely the service. cloudflared exposes a metrics endpoint when started with --metrics 127.0.0.1:2000 (set it in the service's config or arguments). On it, /ready returns 200 while at least one of the connector's edge connections is established and a non-200 otherwise, and /metrics publishes Prometheus counters including the number of healthy connections. A one-line cron job turns that into a heartbeat:
# /etc/cron.d/cloudflared-health — pings only while cloudflared reports ready
* * * * * root systemctl is-active --quiet cloudflared && curl -fsS -m 3 http://127.0.0.1:2000/ready >/dev/null && curl -fsS -m 10 -X POST https://cronalert.com/api/heartbeat/<token> >/dev/null Create the heartbeat with a 1-minute expected interval; with the default grace it alerts after two minutes of silence. If the host runs more than the tunnel, fold the /ready check into the host's existing health script from the VPS guide instead of adding a second heartbeat. Heartbeats are on the Pro plan.
A note on the connections: cloudflared normally keeps four connections to different Cloudflare data centers for redundancy. /ready is satisfied by one. If you want to know when it's limping on fewer than four (a sign of flaky upstream networking that will eventually become a 530), read cloudflared_tunnel_ha_connections from /metrics in the script and fail below two. cloudflared tunnel info <name> shows the same from the command line.
Reading the two together
| Public hostname check | Host heartbeat | Most likely |
|---|---|---|
| 530 | Silent | The host is off or offline. Power, network, provider. |
| 530 | Fine | The host is up but cloudflared isn't connected: stopped, crashed, blocked outbound, or credentials revoked. The heartbeat's /ready check should also be failing; if it isn't, the hostname routes to a different tunnel than you think. |
| 502 | Fine | The service behind the tunnel. Check the container or process and the ingress rule's port. |
| 200, keyword failing | Fine | Something else is answering: an Access login page, or a different service after an ingress change. |
| 200 | Silent | The connector is up but the host's cron or the metrics endpoint isn't; check --metrics is configured. |
Things that take tunnels down
- Updates.
cloudflaredupdates itself by default when installed as a service, and a failed update leaves it stopped. Put a maintenance window over any manual update slot, and let the heartbeat catch the automatic ones. - Rotated or deleted tokens. Deleting a tunnel in the dashboard, or rotating a remotely-managed tunnel's token without updating the host, produces a connector that starts and immediately exits. 530 everywhere.
- Outbound firewall changes. The connector dials out over 7844 (QUIC or HTTP/2). A new egress rule that blocks it looks identical to a crashed daemon from outside.
- Ingress edits. Pointing a hostname at a wrong port or a renamed container produces 502 while every other hostname on the tunnel works. One monitor per hostname is what makes this visible.
- Cloudflare itself. Rare, but a regional Cloudflare incident can 530 a healthy tunnel. If your connector's heartbeat is fine and the hostname is 530, check Cloudflare's status page before the host.
Frequently asked questions
What does 530 / error 1033 mean?
No connected cloudflared for the tunnel. 502 means the tunnel is up and the service behind it isn't.
Why is my Access-protected monitor always up?
CronAlert follows the redirect to the Access login page, which returns 200. Add service-token headers so the request reaches the real service.
How do I monitor a tunnel with no public hostname?
A heartbeat from the host that requests cloudflared's /ready endpoint and pings on success.
Which plan?
HTTP checks with headers are free. Keywords and the heartbeat are Pro ($5/mo).
Does the tunnel hostname's certificate need monitoring?
No; it's Cloudflare's edge certificate. The check tracks it anyway at no cost.
Monitor the hostname, then the connector
A tunnel replaces a port forward with a daemon, and daemons stop. One HTTP monitor per public hostname, with service-token headers where Access is in front, reads Cloudflare's own verdict on whether the tunnel or the origin broke. One heartbeat on /ready tells you about the connector directly, hostname or not. Create a free account, add the hostname checks, and upgrade to Pro for the heartbeat before the next silent update. Related reading: monitoring the reverse proxy the tunnel usually hands off to, monitoring Docker and self-hosted apps, monitoring a Tailscale or WireGuard VPN for the other way of not opening ports, and choosing between HTTP, TCP, and heartbeat monitors.