Vercel cron job not running?
A Vercel cron job that “didn’t run” almost always falls into one of three cases: it was never registered on the production deployment, the request was turned away before your code ran, or it ran at a time you didn’t expect. Open your project’s Settings, Cron Jobs first. If the job isn’t listed, it was never deployed. If it is listed but View Logs shows nothing for the time it should have run, the request was redirected, cached or never delivered.
1. It isn’t on a production deployment
Vercel only runs cron jobs on production. It triggers each one with a GET request to your production deployment URL and the path from vercel.json. A cron added on a branch does nothing until that code is deployed to production.
Check: open the production deployment’s summary. Its cron jobs are listed there, and you can run one by hand from the same place.
2. The cron was never registered
If vercel.json isn’t where the build expects it, or a renamed route no longer matches the cron’s path, the job is either missing or calling a route that no longer exists. A cron pointed at a missing path still runs, and gets a 404.
Check: run vercel build --prod locally and look for a crons entry in .vercel/output/config.json. Then compare each path with your routes.
3. The schedule isn’t what you think
- It’s UTC, always.
0 9 * * *is 09:00 UTC, not 09:00 where you are. - No names. Vercel doesn’t accept
MON,SUN,JANorDEC; use numbers (weekdays are 0 to 6, Sunday is 0). - Day of month or day of week, not both. On Vercel, when one has a value the other must be
*.
Check: paste the five fields into the schedule at the top of cronmiss.com to see the next runs in UTC.
4. Hobby plan limits
On Hobby, a cron job can run at most once a day. A more frequent expression fails the deployment with “Hobby accounts are limited to daily cron jobs. This cron expression would run more than once per day.” A daily job can also fire at any point in its hour: 0 8 * * * may run anywhere from 08:00:00 to 08:59:59. On Pro and Enterprise, jobs run within the scheduled minute, as often as once a minute. Every plan allows 100 cron jobs per project.
5. A redirect ended the request
Cron invocations don’t follow redirects. If middleware sends unauthenticated requests to a login page, or your app redirects to add a trailing slash, the cron gets a 3xx and stops there. Your handler never runs, and redirected invocations don’t appear in the logs.
Fix: let the cron path through your middleware, and if trailingSlash is on, write the cron path with the trailing slash.
6. A cached response answered instead
If the route is cached, the cron can be answered from cache without running your code, and cached responses don’t show in the logs either.
Fix: in a Next.js route handler, add export const dynamic = 'force-dynamic'.
7. CRON_SECRET doesn’t match
When a project has a CRON_SECRET environment variable, Vercel sends it as Authorization: Bearer <secret>. If your check doesn’t match, your own code returns 401 and the job does nothing. A secret with a trailing newline or characters that aren’t valid in a header is a common cause.
Check: regenerate the secret as a plain random string of at least 16 characters, redeploy, and look for 401s in the job’s logs.
8. A firewall rule blocked it
Custom WAF rules can block cron requests before they reach the function. Cron requests always use the user agent vercel-cron/1.0, which you can allow explicitly.
9. Cron jobs are disabled for the project
The Disable Cron Jobs button in the project’s Cron Jobs settings stops every cron in the project. The setting survives promotions and Instant Rollbacks, so a project disabled months ago stays disabled until someone turns it back on.
10. It started but never finished
A cron job has the same maximum duration as any Vercel Function. A job that runs past it is stopped part way through, which often looks like “it ran, but nothing happened”. A job that takes longer than its interval can also overlap with the next run.
Fix: split long work into smaller jobs, set maxDuration deliberately, and use a lock if runs must not overlap.
Why the logs can’t tell you
Vercel says cron delivery is best effort. Occasionally a transient network error stops a scheduled request from reaching your function: the function doesn’t run and no runtime log is created. Vercel doesn’t retry failed invocations either, and redirected or cached invocations are left out of the logs. So the logs describe the runs that happened. They can’t show you the one that didn’t.
The same delivery can also arrive twice, so a job should be safe to run again: process “everything since the last successful run” rather than “add 10 to the balance”.
How to know the next time it doesn’t run
Turn the check around. Instead of looking for an error, have the job report success, and get alerted when that report doesn’t arrive on time. This is a heartbeat, or dead man’s switch. The ping goes on the last line, so it only fires if the job got that far:
// app/api/cron/sync/route.ts
import { ping } from "cronmiss";
export const dynamic = "force-dynamic"; // never answer from cache
export async function GET(request: Request) {
if (request.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}
await syncInvoices(); // your job
await ping(process.env.CRONMISS_ID!); // only reached if the job finished
return Response.json({ ok: true });
}ping never throws, so monitoring can’t fail the job. Without the package, a plain fetch to the ping URL does the same. Set the grace period to fit the plan: on Hobby, the hour Vercel may take plus your job’s duration; on Pro, a few minutes.
cronmiss is in pre-signup and opens with Vercel first. Until then the pattern works with any heartbeat monitor, Healthchecks.io included, and the code above stays the same apart from the URL.
Questions
Do Vercel cron jobs run on preview deployments?
No. Vercel only runs cron jobs on production deployments, by sending a GET request to the production URL with the path in vercel.json.
What timezone do Vercel cron jobs use?
Always UTC. There is no timezone setting, so 0 2 * * * runs at 02:00 UTC whatever region your project is in.
Does Vercel retry a cron job that fails?
No. Vercel does not retry a failed invocation, and delivery is best effort: a scheduled run can occasionally fail to reach your function, in which case there is no runtime log for it.
Can a Hobby project run a cron job more than once a day?
No. On Hobby, expressions that run more than once a day fail deployment, and a daily job can be invoked at any point within its scheduled hour.
Why doesn’t my cron job appear in the logs?
Invocations that end in a redirect or are answered from cache are not shown in the logs, and runs that never reached your function leave no log at all.