Cloudflare sits between the internet and your origin — handling CDN caching, DDoS mitigation, DNS resolution, Zero Trust access, Workers serverless compute, and R2 object storage. That edge position makes it powerful and it makes it a single point of failure. A misconfigured firewall rule that blocks legitimate users, a Workers deployment that throws uncaught exceptions, a cache purge that unexpectedly floods your origin, or a DNS record that resolves to a dead IP — these failures can be invisible from inside Cloudflare's own dashboard until traffic drops sharply. Vigilmon adds external monitoring: HTTP monitors that check your site from the outside, heartbeats for Workers cron triggers, and alerts for the metrics that matter most.
This tutorial covers monitoring your Cloudflare-protected applications and Cloudflare Workers end-to-end.
What You'll Build
- Vigilmon HTTP monitors for your Cloudflare-fronted origin and key routes
- A Workers health endpoint with cache and origin status
- A Workers Cron Trigger that pings a Vigilmon heartbeat on each successful run
- A Cloudflare API poller that surfaces Zone Analytics metrics into a health endpoint
- Monitoring for R2 object storage availability
Prerequisites
- A Cloudflare account with at least one zone (domain) active
- Cloudflare Workers enabled on your account
- A Cloudflare API token with Zone:Read and Account:Read permissions
- A free account at vigilmon.online
Step 1: Monitor Your Cloudflare-Fronted Site
The first thing to monitor is whether your site actually returns the right response through Cloudflare's edge. An HTTP monitor checks this from the outside — it sees what your users see, including any firewall blocks, redirect loops, or origin errors.
Add the primary site monitor
- Log in to Vigilmon → Add Monitor → HTTP.
- URL:
https://<your-domain>/(your Cloudflare-fronted URL) - Check interval: 60 seconds
- Expected status: 200
- Response timeout: 10 seconds
- Label: "Cloudflare — main site"
Verify the Cloudflare header
You can confirm Vigilmon's checks actually go through Cloudflare by looking for CF-Ray in the response headers. Add a header assertion in Vigilmon:
- Header:
CF-Ray - Assertion: present
This catches the rare case where a DNS misconfiguration routes traffic to your origin directly, bypassing the CDN.
Step 2: Deploy a Cloudflare Workers Health Endpoint
Cloudflare Workers run JavaScript/TypeScript on Cloudflare's global edge. A dedicated health Worker can check your origin, return cache status, and probe downstream dependencies — all from the edge.
worker.ts
export interface Env {
ORIGIN_URL: string;
UPSTREAM_API_URL: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/health") {
return handleHealth(env);
}
// Pass all other requests to origin
return fetch(request);
},
};
async function handleHealth(env: Env): Promise<Response> {
const checks: Record<string, string> = {};
let ok = true;
// Check origin availability
try {
const start = Date.now();
const resp = await fetch(`${env.ORIGIN_URL}/ping`, {
signal: AbortSignal.timeout(5000),
});
const latencyMs = Date.now() - start;
checks.origin = resp.ok
? `ok (${latencyMs}ms)`
: `http_${resp.status}`;
if (!resp.ok) ok = false;
} catch (err) {
checks.origin = `error: ${(err as Error).message}`;
ok = false;
}
// Check upstream API
try {
const resp = await fetch(`${env.UPSTREAM_API_URL}/health`, {
signal: AbortSignal.timeout(3000),
});
checks.upstream_api = resp.ok ? "ok" : `http_${resp.status}`;
if (!resp.ok) ok = false;
} catch (err) {
checks.upstream_api = `error: ${(err as Error).message}`;
ok = false;
}
return Response.json(
{
status: ok ? "ok" : "degraded",
checks,
colo: (request as Request & { cf?: { colo: string } }).cf?.colo ?? "unknown",
},
{ status: ok ? 200 : 503 }
);
}
wrangler.toml
name = "my-app-worker"
main = "worker.ts"
compatibility_date = "2024-01-01"
[vars]
ORIGIN_URL = "https://origin.your-app.internal"
UPSTREAM_API_URL = "https://api.upstream.com"
Deploy with wrangler deploy, then add a Vigilmon HTTP monitor:
- URL:
https://<your-domain>/health - Check interval: 60 seconds
- JSON assertion:
status=ok - Label: "Cloudflare Workers — health"
Step 3: Monitor Workers Cron Triggers with a Heartbeat
Cloudflare Workers support scheduled handlers that run on a cron schedule. If your cron trigger silently fails (uncaught exception, account quota, deployment regression), you won't know without an external heartbeat.
worker.ts — add the scheduled handler
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// ... existing fetch handler
return handleHealth(env);
},
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
ctx.waitUntil(runScheduledJob(env));
},
};
interface Env {
ORIGIN_URL: string;
UPSTREAM_API_URL: string;
VIGILMON_HEARTBEAT_URL: string;
}
async function runScheduledJob(env: Env): Promise<void> {
try {
// Your actual scheduled work
await syncAnalyticsToWarehouse();
// Ping Vigilmon heartbeat on success
await fetch(env.VIGILMON_HEARTBEAT_URL, { method: "POST" });
} catch (err) {
// Do NOT ping heartbeat — let Vigilmon fire the alert
console.error("Scheduled job failed:", err);
}
}
wrangler.toml — add the cron trigger
[triggers]
crons = ["0 6 * * *"]
Add the secret via Wrangler:
wrangler secret put VIGILMON_HEARTBEAT_URL
# Paste: https://vigilmon.online/api/heartbeat/<your-heartbeat-id>
In Vigilmon, create a Heartbeat monitor with a grace period of 30 hours for a daily job.
Step 4: Poll Cloudflare Zone Analytics via API
Cloudflare's GraphQL Analytics API exposes cache hit ratios, request counts, bot traffic scores, and firewall event rates. You can query this data from a health endpoint and surface thresholds that Vigilmon can assert against.
cloudflare-analytics.ts
const CF_API_TOKEN = process.env.CF_API_TOKEN!;
const CF_ZONE_ID = process.env.CF_ZONE_ID!;
interface ZoneAnalytics {
cacheHitRatio: number;
totalRequests: number;
firewallTriggeredRequests: number;
errorRatePct: number;
}
export async function getZoneAnalytics(
sinceMinutes = 60
): Promise<ZoneAnalytics> {
const query = `
query {
viewer {
zones(filter: { zoneTag: "${CF_ZONE_ID}" }) {
httpRequests1hGroups(
limit: 1
filter: { datetime_geq: "${new Date(Date.now() - sinceMinutes * 60_000).toISOString()}" }
) {
sum {
requests
cachedRequests
responseStatusMap { edgeResponseStatus value }
}
}
firewallEventsAdaptive(
limit: 1
filter: { datetime_geq: "${new Date(Date.now() - sinceMinutes * 60_000).toISOString()}" }
) {
count
}
}
}
}
`;
const resp = await fetch("https://api.cloudflare.com/client/v4/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${CF_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query }),
signal: AbortSignal.timeout(10_000),
});
if (!resp.ok) throw new Error(`CF API error: ${resp.status}`);
const data = await resp.json();
const groups =
data.data?.viewer?.zones?.[0]?.httpRequests1hGroups?.[0]?.sum;
const totalRequests: number = groups?.requests ?? 0;
const cachedRequests: number = groups?.cachedRequests ?? 0;
const cacheHitRatio =
totalRequests > 0
? Math.round((cachedRequests / totalRequests) * 100)
: 0;
const statusMap: { edgeResponseStatus: number; value: number }[] =
groups?.responseStatusMap ?? [];
const errorRequests = statusMap
.filter((s) => s.edgeResponseStatus >= 500)
.reduce((sum, s) => sum + s.value, 0);
const errorRatePct =
totalRequests > 0
? Math.round((errorRequests / totalRequests) * 100)
: 0;
const firewallCount =
data.data?.viewer?.zones?.[0]?.firewallEventsAdaptive?.[0]?.count ?? 0;
return {
cacheHitRatio,
totalRequests,
firewallTriggeredRequests: firewallCount,
errorRatePct,
};
}
Expose this from a health endpoint:
// app/api/health/cloudflare/route.ts
import { getZoneAnalytics } from "@/lib/cloudflare-analytics";
export async function GET() {
try {
const analytics = await getZoneAnalytics(60);
const ok =
analytics.errorRatePct < 5 &&
analytics.cacheHitRatio > 20;
return Response.json(
{
status: ok ? "ok" : "degraded",
analytics,
},
{ status: ok ? 200 : 503 }
);
} catch (err) {
return Response.json(
{ status: "error", error: (err as Error).message },
{ status: 503 }
);
}
}
Add a Vigilmon HTTP monitor for /api/health/cloudflare with JSON assertions:
status=okanalytics.errorRatePct<5
Step 5: Monitor R2 Object Storage
Cloudflare R2 is S3-compatible. Monitor bucket availability with a HEAD request to a canary object — similar to the Hetzner Object Storage approach.
Upload a canary object via the R2 API or Wrangler:
echo "" | wrangler r2 object put <your-bucket>/health-canary --pipe
Then add a Vigilmon HTTP monitor targeting your R2 public bucket URL:
- Add Monitor → HTTP.
- URL:
https://pub-<hash>.r2.dev/health-canary(your R2 public bucket URL) - Method: HEAD
- Expected status: 200
- Label: "Cloudflare R2 — object storage"
Step 6: Embed the Status Page
Go to Status Pages → Create in Vigilmon, add all your Cloudflare monitors, and embed the badge:
[](https://vigilmon.online/status/<status-page-slug>)
Or embed the live widget:
<script
src="https://vigilmon.online/embed.js"
data-page="<status-page-slug>"
data-theme="light"
async
></script>
Key Metrics to Monitor
| Metric | Source | Vigilmon feature | |---|---|---| | Site availability through CDN | HTTP monitor on domain | 60 s HTTP check with CF-Ray header assertion | | CF-Ray header present | HTTP monitor header assertion | Confirms Cloudflare is actually proxying | | Workers health | HTTP monitor on /health Worker | JSON assertion on status | | Origin response time | Workers health endpoint | JSON assertion on latency | | Workers Cron Trigger | Heartbeat from scheduled handler | Heartbeat monitor with grace period | | Cache hit ratio | Zone Analytics API endpoint | JSON assertion cacheHitRatio > 20 | | 5xx error rate | Zone Analytics API endpoint | JSON assertion errorRatePct < 5 | | Firewall rule spikes | Zone Analytics API endpoint | JSON assertion on firewallTriggeredRequests | | R2 bucket availability | HEAD request to canary object | HTTP monitor expected 200 |
Alerting Recommendations
- Slack webhook — post to
#infrastructurewhen the CDN or Workers health degrades - Email — on-call address for firewall-related traffic spikes (potential attack)
- PagerDuty — for 5xx error rate spikes and origin unreachability
Cloudflare's DDoS protection and caching are automatic, but application-layer failures still need your attention. Set Vigilmon re-notification to every 5 minutes while a monitor is down, and use 2-check confirmation thresholds to avoid alert fatigue from transient edge anomalies.
Cloudflare protects and accelerates your traffic. Vigilmon tells you whether your application is actually healthy behind that protection.
Start monitoring your Cloudflare-protected apps today — register free at vigilmon.online.