Why your Vercel cron job is not running

You added a cron job, deployed, and nothing happened. No run, no error, no log line. On Vercel this is almost never a mystery — it's one of a handful of rules that fail quietly, and they're easy to walk into.
Here are the reasons a Vercel cron job doesn't run, in rough order of how often they catch people — and how to tell which one is yours.
- 1×/day
- max frequency on the Hobby plan
- within the hour
- when it actually fires — not on the minute
- 100
- cron jobs per project, on every plan
- prod only
- crons run on production deployments, not previews
1. You're on Hobby and asked for more than once a day
This is the big one. On the Hobby plan, a cron job can run at most once per day. A schedule like */5 * * * * or 0 * * * * (every five minutes, every hour) isn't throttled — it fails the deployment outright with an error about the schedule.
If your deploy is failing, read the build error. If it deployed but never runs, the schedule that got through is a daily one, running once. To go more frequent you need Pro, or a different scheduler entirely.
2. It runs — just not when you think
A daily cron set for 0 9 * * * does not fire at exactly 09:00. Vercel invokes cron jobs at any point within the scheduled hour to spread load, so "it didn't run at 9" might just be a job that runs at 9:41. Give it the full hour before calling it dead.
3. Your CRON_SECRET check is rejecting the real cron
The recommended way to stop strangers hitting your cron URL is to set a CRON_SECRET environment variable. Vercel then calls your route with an Authorization: Bearer <CRON_SECRET> header, and you reject anything that doesn't match.
Get that comparison slightly wrong — a stray space, the wrong env, the secret set for Preview but not Production — and your own route returns 401 to Vercel on every invocation. The job fires on schedule and fails every single time, which reads in your head as "not running."
4. It's not on a production deployment
Cron jobs only run on your production deployment. Testing on a preview branch, or never promoting the deploy that added the cron, means the schedule simply doesn't exist yet. Ship it to production and confirm the job shows up under the project's cron settings.
A route that fails loudly instead of silently
Most of the pain above comes from a handler that swallows its own errors. Write it so a bad secret or a thrown error returns a real error status, and set the schedule to something Hobby accepts. This prompt does both:
Write a Vercel Cron Job route for my Next.js App Router project and wire up the config. Requirements: - A GET route handler at app/api/cron/<name>/route.ts. - At the top, verify the request came from Vercel Cron: read the Authorization header and compare it to "Bearer " + process.env.CRON_SECRET. If it doesn't match, return a 401 immediately. - Only run the actual job after that check passes. Wrap the work in try/catch, log any error clearly, and return a 500 on failure so a broken run shows up instead of a fake 200. - Give me the matching vercel.json "crons" entry with a once-per-day schedule (so it's valid on the Hobby plan) and explain how to change it if I upgrade to Pro. - Remind me to set CRON_SECRET in my Vercel project's environment variables for Production, and that cron jobs only run on production deployments. Show me both files in full.
For the full rules straight from the source, Vercel's cron jobs documentation lists the per-plan limits.
Catching the runs Vercel won't tell you about
Even a correct cron fails eventually — a downstream API dies, the secret gets rotated, a deploy breaks the route. Vercel schedules the job; it doesn't judge whether your run succeeded. That part is on you.
The durable fix is the same one that works for any scheduled job: have it ping a heartbeat on success and get alerted when the ping goes missing. Tell Me When Down watches for that silence, so a cron that started 500-ing at 3am reaches you before your users do.
Know when your cron fails — not just when Vercel fires it.
Join Tell Me When Down free and drop one heartbeat ping at the end of your job. If a run 401s, 500s, or never fires, you get an email in minutes instead of finding out from a missed charge or an empty table.
spot something wrong or out of date? [email protected] — we'll fix it