Monitor Vercel Cron Jobs

Vercel doesn't retry a failed cron invocation, and delivery is best effort, so a run can be missed without anything in your logs. Ping a URL when the work completes and we email you when a run is late or fails.

You need: a Lateping account, a Vercel project with a cron job, about 5 minutes.

What can go wrong

A Vercel Cron Job is a scheduled GET request to one of your routes. When that request fails or never arrives, the only trace is in the function logs, if there is one at all.

Set it up

  1. Create a check

    Create a check in Lateping with the same cron expression as the job and the timezone UTC (Vercel cron schedules run in UTC). Copy its ping URL into LATEPING_URL in your project’s environment variables.

    On the Hobby plan, Vercel may run a daily job at any point within the scheduled hour, so set the grace period to at least an hour plus the job’s run time. On Pro and Enterprise, jobs start within the scheduled minute.

  2. Ping from the route

    vercel.json
    {
      "crons": [{ "path": "/api/cron/nightly", "schedule": "17 2 * * *" }]
    }
    
    app/api/cron/nightly/route.ts
    export async function GET(request: Request) {
      // Vercel sends CRON_SECRET as a bearer token. Reject anything else.
      if (request.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
        return new Response("Unauthorized", { status: 401 });
      }
    
      await ping("/start");
      try {
        await runJob();
      } catch (err) {
        await ping("/fail");
        throw err; // keep the invocation marked as failed in Vercel's logs
      }
      await ping("");
      return Response.json({ ok: true });
    }
    
    async function runJob() {
      // Your scheduled work goes here. Await everything it starts.
    }
    
    async function ping(path: string) {
      try {
        const res = await fetch(process.env.LATEPING_URL + path, {
          method: "POST",
          signal: AbortSignal.timeout(10_000),
        });
        if (!res.ok) console.error(`Lateping ping ${path || "/"} returned ${res.status}`);
      } catch (err) {
        console.error(`Lateping ping ${path || "/"} failed: ${err}`);
      }
    }
    
    • Set CRON_SECRET to a random string of at least 16 characters. Without the check, anyone who finds the route can run your job and send pings.
    • The success ping is sent only after runJob finishes without throwing. If it throws, /fail alerts straight away.
    • If the invocation never arrives, or the function times out before the last line, no success ping comes and the check alerts after the grace period.
    • ping() never throws, so a network problem reaching Lateping can’t fail a job that worked.
  3. Test it

    Cron jobs only run on production deployments, and there’s no local cron runner. Deploy, then call the route yourself with the secret:

    bash
    curl -H "Authorization: Bearer $CRON_SECRET" https://your-app.vercel.app/api/cron/nightly
    

    The check should show a start ping and a success ping.

Notes

  • Await the pings. Once the function returns, the platform can stop it before an un-awaited request is sent.
  • Keep the job inside the function’s maximum duration. A run that hits the limit is killed before its success ping, which is what you want to hear about.
  • If a job can run longer than its interval, Vercel may start a second run while the first is still going. Use a lock, or give the check a longer grace period and fix the overlap.
  • One check per schedule. If several schedules share a path, branch on the x-vercel-cron-schedule header and give each its own check URL.
  • Alerts go by email. On Pro and above they can also go to Slack, Discord or a webhook.

Next steps

On this page