Skip to main content

Vercel Cron Job Not Running: The Failures That Look Like Successes

A Vercel cron job not running often looks like it ran. The redirect and caching traps, why the logs stay empty, and how to catch a miss reliably.

FLAREWARDEN
FlareWarden Team
7 min read

Your vercel.json has the schedule. The deployment succeeded. The dashboard shows the cron job. And the thing the job was supposed to do never happened. Before rewriting any code, it’s worth knowing that Vercel treats several distinct outcomes as “the job ran,” and two of them involve your handler never executing at all.

First, Separate “Never Fired” From “Fired and Did Nothing”

These are different problems with different evidence, and conflating them is why people spend an afternoon debugging working code. A Vercel cron job not running falls into one of two buckets:

  • It never fired. Configuration or plan limits stopped the invocation from happening.
  • It fired and your code didn’t run. Vercel made the request, got a response it considered final, and moved on.

Work through the first bucket, because it’s cheap to rule out.

The Configuration Tier

Vercel triggers a cron job by making an HTTP GET request to your project’s production deployment URL, using the path from vercel.json. Three things break that before your code is involved.

  1. You’re on a Preview deployment. Cron jobs only run on Production deployments. A job that works on your branch preview and vanishes in production, or the reverse, is usually this.
  2. The plan doesn’t allow the schedule. As of July 2026, Hobby accounts are limited to cron jobs that run once per day, and expressions that would run more often fail during deployment with an explicit error: Hobby accounts are limited to daily cron jobs. Pro and Enterprise allow once per minute. All three tiers cap out at 100 cron jobs per project.
  3. CRON_SECRET doesn’t match. If your handler rejects the request because the secret is wrong, malformed, or carries a stray newline, the job “ran” and returned 401. Vercel’s own troubleshooting guide lists validating that variable as an early step.

There’s also a precision trap worth knowing before you conclude anything from a clock. Hobby scheduling precision is per-hour, ±59 minutes; Pro and Enterprise are per-minute. A Hobby job scheduled for midnight can legitimately fire at 00:58, so “it didn’t run at midnight” is not evidence of a failure. It’s evidence you’re on Hobby.

The Silent Tier: Redirects and Caches

Here’s where the interesting failures live, and both are documented behaviors rather than bugs.

Cron jobs do not follow redirects. When a cron-triggered endpoint returns a 3xx, the job completes without further requests, and Vercel treats the redirect response as final. Your handler at the redirect target never runs. The most common trigger is the trailingSlash option: with it enabled, a cron path missing its trailing slash gets a redirect, and your browser follows it while the cron invocation does not. Which is exactly why testing the URL by hand proves nothing. Add the trailing slash and the redirect disappears.

A cached response means the function never executes. If you’re not seeing logs for a cron job, caching is a likely cause: Vercel serves a cached response instead of invoking the function. Adding export const dynamic = 'force-dynamic'; to the route handler forces execution on every request. Until you do, the schedule fires, something returns 200, and nothing runs.

Both failures produce the same reading on every instrument you have. The job is listed. The invocation happened. No error appears anywhere. The work silently doesn’t get done, for days, until something downstream is visibly stale.

Why the Logs Can’t Settle It

Cron jobs are logged as function invocations, viewable from the Logs section of the project dashboard or via View Logs on the Cron jobs settings page. That works well when the function actually runs. It’s structurally unable to help with the two silent failures, because a cached response and a redirect both mean there was no function invocation to log. Absence of a log entry is ambiguous: it means either “your code didn’t run” or “your code ran and logged nothing.”

Local testing doesn’t close the gap either, since cron jobs aren’t supported under vercel dev or next dev. The schedule only exists in production, which is the one environment where you’re not watching.

Trusting logs you don’t read on a schedule is a classic monitoring anti-pattern: the data technically exists, and nobody is looking at 3 a.m.

The Fix: Make the Job Report Itself

Every failure above shares one property. In each case, your handler did not execute. So put the proof of execution inside the handler and alert on its absence. That’s a dead man’s switch, and it collapses five diagnostic paths into one signal.

// app/api/cron/route.ts
export const dynamic = 'force-dynamic';

export async function GET(request: Request) {
  if (request.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response('Unauthorized', { status: 401 });
  }

  const ping = 'https://app.flarewarden.com/ping/YOUR-TOKEN';
  await fetch(`${ping}/start`);

  try {
    await doTheActualWork();
    await fetch(`${ping}/complete`);
    return new Response('ok');
  } catch (err) {
    await fetch(`${ping}/fail`);
    throw err;
  }
}

The reason this catches the silent tier is that a cached response or a redirect means these lines never run, so the ping never arrives, so the monitor goes Late and alerts. You don’t have to have predicted the failure mode. Absence covers all of them, including the ones nobody has documented yet.

In FlareWarden, open the parent uptime monitor for the project, click Add Cron Monitor, and set a name, the expected schedule, and a grace period. You get a unique ping URL back, and the UUID in it is the credential, so there are no API keys to manage. Nothing needs inbound access to Vercel: the ping is an outbound HTTPS request from a function you already run. The cron monitors documentation covers the /start, /complete, and /fail lifecycle, which is what turns “didn’t run” and “started but hung” into two distinguishable alerts rather than one vague one.

Set the grace period against your plan’s precision, not your schedule. On Hobby, a daily job can arrive up to 59 minutes late by design, so a 15-minute grace period will page you for normal behavior. Give it 90 minutes and you’ll hear only about real misses. On Pro, where precision is per-minute, a much tighter grace period is honest.

When You Can Skip All This

Not every scheduled job earns a monitor. A cache warmer that runs hourly and fails twice a month costs nothing, and wiring an alert to it just adds noise to compete with the alerts that matter. The line we’d draw: monitor the jobs that move money, send customer-facing mail, or write data that something else reads later. Skip the ones whose failure self-corrects on the next run.

The same argument applies beyond Vercel. Any platform whose scheduler discards output has this blind spot, which is why the pattern looks identical when you monitor Laravel scheduled tasks or a Kubernetes CronJob. Cron and heartbeat monitoring is included on every plan, free included, because a scheduled job you can’t verify isn’t really scheduled.


Key Takeaways

  • Rule out configuration first — Production-only invocation, Hobby’s one-run-per-day limit, and a mismatched CRON_SECRET account for most jobs that never fire at all.
  • Redirects and caches are the silent killers — a 3xx is treated as final and a cached response skips the function entirely, so the invocation “succeeds” while your code never runs.
  • Logs can’t prove a negative — no log entry means either nothing ran or nothing logged, and cron isn’t supported under vercel dev to test locally.
  • Ping from inside the handler — proof of execution placed where the work happens catches every failure mode at once, documented or not.
  • Size the grace period to the platform’s precision — Hobby’s ±59-minute window needs roughly 90 minutes of grace; per-minute Pro schedules can be far tighter.

Want to know when a scheduled job goes quiet instead of finding out from stale data? Start monitoring free with FlareWarden — cron and heartbeat monitors on every plan, 15 monitors, no credit card required.