This one is personal: CronAlert is an Astro app — SSR on Cloudflare, a static landing page, HTMX islands of interactivity — and we monitor it with itself. So this guide isn't a framework tour. It's field notes on how Astro deployments actually fail, including one production failure mode (a security default silently 403ing webhooks) that we found the hard way.
Astro's defining trait for monitoring purposes: it's static by default, server-rendered by exception. That's the opposite of Next.js and Nuxt, and it means most Astro sites start life with static-site failure modes and grow server-side ones page by page. Your monitoring should grow the same way.
Start from your output mode
- Fully static (the default). Every page is built ahead of time and served from a CDN. There is no server to crash — failures live in DNS, TLS, CDN configuration, and bad deploys. The static-site monitoring guide applies wholesale: homepage monitor, one deep page, certificate expiry, and a keyword check that proves the build shipped what you think it shipped.
- Hybrid. Mostly prerendered, with
export const prerender = falseon the pages and endpoints that need a server. Now you have two failure axes at once, and the prerendered/SSR pair is diagnostic: if the static page is up and the SSR page is down, it's your runtime or data layer; if both are down, look at DNS/CDN. - Server output. Every route hits the adapter runtime per request. Failures are the classic ones — data layer down, runtime limits, middleware exceptions — and they show up as real 500s an HTTP monitor sees immediately.
One refactor-shaped gotcha: a page's rendering mode is a one-line export, which means it can change in a one-line diff. A page that quietly flips from prerendered to SSR inherits a whole new failure catalog without anyone deciding that on purpose. Monitoring one page per mode means the flip shows up as a changed failure pattern instead of a mystery.
The monitor set
1. A health endpoint with dependency checks
Astro API routes make this one file. Here's the Cloudflare-adapter flavor we use (swap the D1 probe for your own database client on other adapters):
// src/pages/api/health.ts
import type { APIRoute } from "astro";
export const prerender = false;
export const GET: APIRoute = async ({ locals }) => {
const checks = { database: "unknown" };
let healthy = true;
try {
await locals.runtime.env.DB.prepare("SELECT 1").first();
checks.database = "ok";
} catch {
checks.database = "down";
healthy = false;
}
return new Response(
JSON.stringify({ status: healthy ? "ok" : "degraded", checks }),
{
status: healthy ? 200 : 503,
headers: { "Content-Type": "application/json" },
}
);
}; The usual health-endpoint rules apply: hard dependencies gate the status code, keep the checks cheap because this runs every few minutes, and add a keyword assertion on "status":"ok" so a CDN's friendly 200 error page can't impersonate health.
2. One page per rendering mode
A static page, and — if you have them — an SSR page that exercises your real data layer. When something breaks, the pattern of greens and reds tells you which layer failed before you've opened a dashboard.
3. The endpoints behind your islands
Astro's islands architecture is great for performance and quietly awkward for monitoring: the server sends finished HTML (which is what your monitor sees), then islands hydrate client-side (which your monitor cannot see — an HTTP check doesn't execute JavaScript). If an island's bundle 404s after a bad deploy or throws during hydration, the page is a healthy 200 with a dead button on it.
You close that gap from two directions:
- Monitor what the islands talk to. Every form action, HTMX endpoint, or fetch target behind your interactive components is an API route you can monitor directly. If the island's backend is up and the page's HTML is correct, the failure surface left over is small.
- Assert on server-rendered content. A keyword check on content Astro renders on the server catches half-broken builds and template errors. Be honest with yourself about the boundary: it verifies the HTML, not the hydration. That boundary is exactly why the API monitors matter.
Middleware: one file, every request
src/middleware.ts runs on every server-rendered request. An exception there — a throwing auth lookup, a bad import, a session-store hiccup being treated as fatal — takes down every SSR route at once while your prerendered pages sail on, green and oblivious. It's the same global single-point-of-failure as hooks.server.ts in SvelteKit or middleware in Next.js. Your SSR page monitor is the tripwire for the whole class; the static/SSR split in your monitor set is what makes the blast radius legible from the outside.
The CSRF default that 403s your webhooks
Here's the one we learned in production. Astro's security.checkOrigin — on by default for server output — rejects cross-site POSTs with form-like content types. That includes text/plain. Browser form submissions are unaffected, JSON APIs are unaffected, and every page you click through works perfectly. But an external service POSTing webhooks to your app with the wrong content type gets a silent 403 Forbidden.
For us it was AWS SNS, whose notifications arrive as text/plain: our bounce-handling endpoint returned 403 to every delivery while the entire site stayed green. (We moved that endpoint to a plain Cloudflare Worker outside the Astro app.) Stripe webhooks survive only because Stripe sends application/json.
The monitoring lesson generalizes: a webhook receiver that's failing doesn't look like downtime — it looks like silence. Test webhook endpoints with the exact content type the real sender uses, and monitor the outcome of the pipeline rather than the page in front of it — a webhook-receiver monitor or a health check that verifies deliveries were processed recently. Your pages being up says nothing about whether Astro is quietly bouncing your integrations.
Adapter-specific failure modes
- Node adapter: you own a long-running process — restart policy, reverse-proxy configuration, and slow memory leaks that appear as a rising response-time trend before they're an outage. The Node.js guide's catalog applies.
- Vercel/Netlify adapters: no process to die; instead cold starts (set timeout thresholds with headroom, use consecutive-check confirmation) and per-function duration ceilings on data-heavy routes.
- Cloudflare adapter (our stack): CPU-time limits, and — the big one — runtime bindings. Env vars and D1/KV bindings live in the dashboard, not just your repo config; the build passes without them and the app 500s only at runtime, on the routes that touch the missing binding. This is configuration drift that no CI step can catch, and it's precisely the failure class external monitoring exists for. The Cloudflare Workers guide goes deeper.
Set it up in ten minutes
- Create a CronAlert account — a typical Astro app needs five to eight monitors, well inside the free plan (25 monitors, 3-minute checks). Keyword checks are Pro at $5/mo.
- Add
src/pages/api/health.tsand point a monitor at it. - Add one monitor per rendering mode you use, plus the endpoints behind your interactive islands.
- If your app receives webhooks, test them with the sender's real content type — then monitor the pipeline's outcome, not just the endpoint.
- Route alerts to Slack or email, and run a fire drill before you trust it.
Frequently asked questions
What should I monitor in an Astro app?
Static sites: homepage, one deep page, TLS. Add SSR and you add a health endpoint, one server-rendered page, and the API routes behind your islands.
Why does my broken page still return 200?
Islands hydrate after the status code is sent, and HTTP monitors don't execute JavaScript. Monitor the islands' APIs and assert on server-rendered content.
How do I add a health endpoint?
An API route at src/pages/api/health.ts with prerender = false, probing hard dependencies and returning 503 when one is down. Code above.
Does the adapter change monitoring?
Yes — Node means process-level concerns, serverless adapters mean cold starts and timeouts, Cloudflare means CPU limits and dashboard-managed bindings that can drift.
Why do POSTs to my app 403 in production?
Astro's default CSRF protection rejects cross-site POSTs with form-like content types, including text/plain. Webhook senders that don't POST JSON get silently blocked — test with the real content type.
Monitor the app you deployed, not the one in your head
Astro's static-by-default posture means many teams under-monitor it ("it's just a static site") right up until the first prerender = false export, the first island with a backend, the first webhook receiver. Read your output modes, place one monitor per mode, watch the endpoints behind the islands, and remember that the greenest dashboard in the world can't see a 403'd webhook. Set up CronAlert free — it's how we watch the Astro app you're reading this on.
Related reading: static site monitoring, Cloudflare Workers monitoring, Next.js monitoring, and designing health check endpoints.