Vercel Cron Job Not Running? 7 Causes and How to Fix Them
Your Vercel cron job never fires, or fires with no logs? Check the seven usual causes in order: production only, plan limits, caching, redirects, middleware, the secret check, and UTC.
A Vercel cron job that does not run is almost always one of seven things: you are looking at a preview deployment, your plan does not allow the schedule, the route is cached, the route redirects, middleware or a firewall blocks it, your own secret check returns 401, or the schedule is in UTC and you read it as local time. Work through them in that order and you will find it in a few minutes.
Vercel cron is just an HTTP GET that Vercel sends to a path in your app on a schedule. That means every problem is a normal request problem. If you treat it that way, the debugging gets simple. This guide follows Vercel's own troubleshooting guide and the most common threads on the Vercel community forum. We could not open vercel.com pages from our writing environment, so the details below come from search result summaries of those pages. Check the current docs before you rely on a limit.
Step 1: Confirm You Are Looking at Production
Cron jobs only run on production deployments. A preview deployment will never fire them, even though the crons entry is in the same vercel.json.
Check three things:
- The deployment you are looking at is the current Production deployment.
- Your latest
vercel.jsonchange was actually deployed to production, not just a branch. - The path in
vercel.jsonmatches a real function in the deployment summary.
{
"crons": [
{
"path": "/api/cron/daily-report",
"schedule": "0 5 * * *"
}
]
}
Vercel's guide also suggests running vercel build --prod locally and checking that .vercel/output/config.json contains a crons property. If it is missing, the file is in the wrong place or has a syntax problem.
Step 2: Check Your Plan's Limits
On the Hobby plan, cron jobs can run at most once per day. A schedule that runs more often is rejected at deploy time. Hobby timing is also loose: according to the usage and pricing page, a job set for 0 1 * * * can fire at any point between 1:00 and 1:59. Pro and Enterprise accounts get per-minute precision.
So "it did not run at 1:00" on Hobby may just mean "it has not run yet". Wait out the hour before you debug further. Old blog posts from 2023 and 2024 that say Hobby allows hourly jobs are out of date.
If you need something more frequent on Hobby, trigger the same route from an external scheduler or upgrade. One community answer also noted that the job count limit applies across the account, not per project, so check that too.
Step 3: Stop the Route From Being Cached
A cron request is a normal GET. If Next.js treats your route handler as static, Vercel may serve a cached response, your code never runs, and there are no logs. This is the most common reason for "cron shows success but nothing happened". A Vercel staff reply in this thread points at exactly this.
Force the route to run every time:
// app/api/cron/daily-report/route.ts
export const dynamic = 'force-dynamic'
export async function GET(request: Request) {
// ...
return Response.json({ ok: true })
}
Look at your build output. If the route is listed as static, that is your answer. Our post on the Next.js "Dynamic server usage" error explains what makes a route static or dynamic.
Step 4: Make Sure the Route Does Not Redirect
Vercel treats a 3xx response as the final result of the invocation and does not follow it. Your handler never runs, and the job looks "done".
Common sources of a hidden redirect:
trailingSlash: trueinnext.config.js, which redirects/api/cronto/api/cron/. Put the trailing slash in the cron path.- A redirect rule for
httptohttpsor for a bare domain towww. - Auth middleware that sends unauthenticated requests to
/login.
Test it yourself with the exact path:
curl -i https://your-app.com/api/cron/daily-report
You want to see a 200 (or your own 401), not a 301, 307 or 308.
Step 5: Check Middleware and the Firewall
Middleware runs before your route. A matcher that is too wide can block or rewrite the cron request. Vercel staff often ask about middleware first in these threads. Exclude your cron path from the matcher, or let it through explicitly:
// middleware.ts
export const config = {
matcher: ['/((?!api/cron|_next/static|_next/image|favicon.ico).*)'],
}
Custom WAF rules and Attack Challenge Mode can also block the request, according to Vercel's guide. If you recently turned either on, that is a strong suspect.
Step 6: Verify the Secret Check
Vercel sends your CRON_SECRET environment variable as an Authorization: Bearer <secret> header, per the manage cron jobs docs. Most apps check it so nobody else can hit the endpoint. The job then "runs" but your code returns 401.
export async function GET(request: Request) {
const secret = process.env.CRON_SECRET
if (!secret || request.headers.get('authorization') !== `Bearer ${secret}`) {
return new Response('Unauthorized', { status: 401 })
}
await runDailyReport()
return Response.json({ ok: true })
}
Guard against an unset secret. Without the !secret check, an empty value can compare equal to Bearer undefined. If the variable is only set for Preview or Development in the dashboard, production gets undefined and every call fails. Remember that a changed environment variable needs a new deployment to take effect, as we cover in Next.js environment variables undefined in production.
Step 7: Convert Your Schedule to UTC
Cron schedules on Vercel are UTC. If you wrote 0 9 * * * meaning 9 am in Mumbai or New York, it will fire at 9:00 UTC. Convert it first, and remember daylight saving does not move a UTC schedule.
Cron duration limits are the same as for Vercel Functions, so a long job can be killed halfway. If your job does real work, read Vercel FUNCTION_INVOCATION_TIMEOUT (504). Split large jobs into batches or hand them to a queue.
Make Failures Visible
After the cause is fixed, make sure you hear about the next failure. A cron job has no user waiting on it, so nobody notices when it stops.
- Log a start line and an end line with counts, such as rows processed.
- Open Runtime Logs, filter by the cron path, and confirm one entry per expected run.
- Ping an external check after each successful run, and alert if the ping stops arriving.
Several users in the forum threads report jobs that only ran after a new build and then went quiet. If that matches your case and every step above checks out, open a support ticket with your deployment URL and the missing run times.
How Relia Helps
A silent cron job is the kind of failure that only shows up when a customer asks why their report never arrived. Relia watches the failing requests, the API and the database of a live product. It groups related signals into one incident explained in plain words, and shows who was affected. You can also add uptime checks on key pages and a public status page. Set up on Node.js and browser apps with npx relia-sdk@latest init. There is a Free plan, and you can see a live demo at app.tryrelia.com/demo.
FAQ
Why does my Vercel cron job show success but nothing happened?
Usually the route was cached or redirected, so your code never ran. Add export const dynamic = 'force-dynamic', and curl the exact path to check for a 3xx.
Do Vercel cron jobs run on preview deployments?
No. They only run on production deployments.
Why does my Vercel cron job run at the wrong time?
Schedules are in UTC. On Hobby, the run can also happen anywhere inside the scheduled hour.
How often can a Vercel cron job run on the free plan?
Once per day on Hobby. More frequent schedules are rejected when you deploy.
Why does the Run button in the dashboard return 200 but my code does not execute?
The same caching problem can apply to a manual run. Check Runtime Logs for an actual invocation, not just the status code.