Skip to content

Monitor Vercel Cron Jobs

Vercel Cron Jobs call a route in your project on a schedule. There’s no server to log in to, so when the route starts failing, times out, or the cron entry disappears in a refactor, nobody notices. A heartbeat check does: the route reports to Pingstack after its work succeeds, and Pingstack alerts you when that report doesn’t arrive on time.

1. Create a heartbeat check that matches the schedule

Section titled “1. Create a heartbeat check that matches the schedule”

Find the job in vercel.json:

{
"crons": [
{ "path": "/api/cron/nightly", "schedule": "0 2 * * *" }
]
}

Create a heartbeat check and set its schedule to Cron with the same expression. Vercel runs cron jobs in UTC, so choose UTC in Pingstack. See schedules and timezones.

Set the grace period with your plan’s timing in mind. On Vercel’s Hobby plan, a daily cron job can run at any point within its scheduled hour, so a 0 2 * * * job may start as late as 02:59. Allow at least an hour there. On paid plans, a grace period covering the function’s runtime plus a few minutes is enough. Attach a notification group.

2. Store the ping URL as an environment variable

Section titled “2. Store the ping URL as an environment variable”

Copy the location’s ping URL from the check in Pingstack. In your Vercel project, open Settings › Environment Variables and add PINGSTACK_NIGHTLY_URL for the Production environment. Cron jobs only run on production deployments.

While you’re there, set a CRON_SECRET if you haven’t. Vercel sends it with each cron request, so your route can reject anyone else who calls it.

In a Next.js App Router project:

app/api/cron/nightly/route.ts
export async function GET(request: Request) {
if (request.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
await sendNightlyReport();
const url = process.env.PINGSTACK_NIGHTLY_URL;
if (url) {
await fetch(url, { signal: AbortSignal.timeout(10_000) }).catch(() => {});
}
return Response.json({ ok: true });
}

The report runs only after sendNightlyReport() resolves, so a run that throws sends nothing and Pingstack alerts you when the deadline passes. await the report before returning: work started after the response is sent may not finish.

Deploy to production, then trigger the job by hand. In your Vercel project, open Settings › Cron Jobs and choose Run next to the job. Open the check in Pingstack and confirm the location received a heartbeat and shows a next deadline.

Add a count the job produces as a numeric value:

const sent = await sendNightlyReport();
const target = new URL(process.env.PINGSTACK_NIGHTLY_URL!);
target.searchParams.set('value', String(sent));
await fetch(target, { signal: AbortSignal.timeout(10_000) }).catch(() => {});

Set warning and critical thresholds on the check, so a run that succeeds but sends nothing still raises an alert.

  • Hobby plan timing. A daily job can start anywhere in its hour. Keep the grace period at an hour or more.
  • Function timeouts. A job that outgrows the function’s maximum duration is cut off before it reports. Raise maxDuration or split the work.
  • Preview deployments. Cron jobs don’t run on previews, and environment variables scoped only to Preview aren’t available in production.
  • Removed cron entry. Deleting the entry from vercel.json stops the job on the next deploy. Pingstack alerts, which is the point.

If alerts still don’t match what you expect, see troubleshooting monitoring.