Skip to content

Blog

Monitoring a Next.js app on Vercel: health route, keyword check and cron heartbeats

Vercel keeps the platform up. Whether your app works, whether the database behind it answers and whether your scheduled functions run is still your job. Here is a setup that covers all three.

Updated 25 October 2026 · By the Upcertain team

1. Add a health route that does real work

A static page proves the CDN answers. A health route proves the app can reach what it depends on. Keep it small, uncached and free of customer data:

// app/api/health/route.ts
import { db } from "@/lib/db";

export const dynamic = "force-dynamic";

export async function GET() {
  try {
    await db.query("select 1");
    return Response.json({ status: "ok" }, { headers: { "cache-control": "no-store" } });
  } catch {
    return Response.json({ status: "error" }, { status: 503 });
  }
}

force-dynamic makes sure the route runs on every request. Next.js 14 and earlier rendered such GET handlers once at build time, which would make the check meaningless.

2. Monitor it

MonitorTargetSettings
Keywordhttps://yourapp.com/api/healthMust contain "status":"ok", every 30 s or 1 min
HTTPhttps://yourapp.com/Expected 200, every 1 min
KeywordYour sign-in pageMust contain a label that only renders when it works

Every HTTPS monitor also checks the certificate and warns before expiry, which covers the custom domain on your Vercel project. Monitor the production domain, not *.vercel.app preview URLs, which change with every deploy.

3. Heartbeats for Vercel cron jobs

Vercel runs scheduled functions from the crons list in vercel.json by calling a path in your app. Hourly schedules like the one below need a paid Vercel plan; on Hobby, crons run at most once a day. If the function throws, times out or the schedule is removed in a refactor, nothing tells you. Report in at the end of the handler:

// vercel.json
{ "crons": [{ "path": "/api/cron/sync", "schedule": "0 * * * *" }] }

// app/api/cron/sync/route.ts
export async function GET() {
  await syncEverything();
  await fetch(process.env.UPCERTAIN_HEARTBEAT_URL!, { signal: AbortSignal.timeout(10_000) });
  return Response.json({ ok: true });
}

Create a heartbeat monitor with a one-hour period and some grace, and put its URL in an environment variable. If the sync stops running or starts failing before the last line, the heartbeat stops and you hear about it.

4. Tune for serverless

  • Cold starts: the first request after a quiet period can be slow. A 10 to 15 second timeout avoids false failures without hiding real hangs.
  • Deploys: Vercel switches traffic to the new deployment when it is ready, so deploys rarely fail checks. If yours do, add a confirmation check.
  • Protect the cron route: when CRON_SECRET is set, Vercel sends it as Authorization: Bearer <CRON_SECRET>; check it so only Vercel can trigger the job. The heartbeat call itself needs no secret.

5. Tell users

Put the app, the API and the scheduled work on a status page at status.yourapp.com. When something fails at 3 a.m., users find the answer there instead of in your inbox.

Questions

Should the health route check every dependency?
Check what the app cannot work without, usually the database. Checking optional services such as analytics turns their outages into yours.
Can I monitor preview deployments?
You can, but their addresses change with each deploy. Monitor production, and run your tests against previews instead.
How often should the health route be checked?
Every 30 seconds to 1 minute for an app customers pay for. Each check is one small database query, which most databases will not notice.