Cron Jobs on Vercel to Trigger a Scheduled Model Task
10 min read · updated August 11, 2026
A Vercel cron job is not a scheduler running your code. It is an HTTP GET that Vercel sends to a path on your production deployment. Every property that surprises people about it — no retries, possible duplicates, no redirects, no local execution — follows from that one fact.
What a Vercel cron actually is
Vercel documents the mechanism precisely: to trigger a cron job it makes an HTTP GET request to your project’s production deployment URL, using the path from vercel.json. The request always carries the user agent vercel-cron/1.0, and an x-vercel-cron-schedule header containing the cron expression that triggered it.
So your endpoint is an ordinary route that anyone on the internet can also call, which is why securing it is a step rather than an afterthought. Cron expressions use standard five-field syntax and the timezone is always UTC. Vercel documents two restrictions: alternative expressions like MON or JAN are not supported, and day-of-month and day-of-week cannot both be set — when one has a value the other must be *.
Plan limits before you design
Vercel’s cron usage and pricing page documents these, and the Hobby row changes what you can build:
Plan Crons/project Minimum interval Precision Hobby 100 once per day per-hour (±59 min) Pro 100 once per minute per-minute Enterprise 100 once per minute per-minute
On Hobby, a more frequent expression does not run late — it fails at deploy time, with an error Vercel quotes as Hobby accounts are limited to daily cron jobs. This cron expression would run more than once per day. And a daily job on Hobby fires anywhere within its hour: Vercel’s example is that 0 1 * * * triggers between 01:00 and 01:59. For anything where the timing carries meaning, that is disqualifying on its own.
Cron jobs invoke Vercel Functions, so function limits apply unchanged — including the duration limits. A nightly job that processes a thousand documents through a model will not fit in one invocation.
Building it
- Add the schedule. The
cronsarray invercel.jsontakespathandschedule:{ "$schema": "https://openapi.vercel.sh/vercel.json", "crons": [ { "path": "/api/cron/digest", "schedule": "0 6 * * *" } ] } - Set
CRON_SECRET. Vercel documents that adding an environment variable of that name causes its value to be sent automatically as anAuthorizationheader on cron invocations, with aBearerprefix, and recommends a random string of at least 16 characters:openssl rand -base64 32 | vercel env add CRON_SECRET production
- Write the route, rejecting anything unauthenticated. Vercel’s own example compares the header to the environment variable and returns 401 on mismatch. Checking the user agent instead is not a substitute — a header anyone can set is not a credential:
// app/api/cron/digest/route.ts export const maxDuration = 300; export async function GET(request: Request) { const authHeader = request.headers.get("authorization"); const cronSecret = process.env.CRON_SECRET; if (!cronSecret || authHeader !== "Bearer " + cronSecret) { return new Response("Unauthorized", { status: 401 }); } const pending = await findWorkSinceLastSuccessfulRun(); let processed = 0; for (const item of pending) { if (await alreadySummarised(item.id)) continue; const summary = await summariseWithModel(item); await storeSummary(item.id, summary); processed += 1; } return Response.json({ ok: true, processed }); } - Deploy to production. The schedule is created from the deployment, so a cron in
vercel.jsonon a branch does nothing until that branch reaches production. Confirm it in the project’s Cron Jobs settings pane, which lists the active jobs and links to their runtime logs. - Test by calling the endpoint directly. Vercel documents that there is no support for cron in
vercel dev,next devor other framework dev servers — a cron job is just a route, so exercise it withcurland a manually supplied Authorization header.
Best-effort delivery is a design input
Vercel states two things about delivery that most cron tutorials omit, and both change the code you should write. Delivery is best effort: occasional transient network errors can prevent a request reaching your function, in which case it does not execute and no runtime log is created for that run. And delivery can occasionally invoke the same scheduled run more than once.
Vercel’s own guidance is that jobs should be idempotent and reconciliation-based, with its worked contrast being “set user status to active” (safe twice) against “increment user credit by 10” (not). For a model job the stakes are literal money: a duplicated run that re-summarises a thousand documents is a second invoice from your provider.
The route above is written for this. It asks what work is outstanding since the last successful run rather than assuming it owns the last interval, so a missed run is caught up automatically; and it checks whether each item is already summarised, so a duplicate run is a series of cheap no-ops. Vercel additionally documents that it will not retry a failed invocation, which makes the reconciliation query the only recovery mechanism you have.
For overlap — a job that runs longer than its own interval — Vercel suggests a distributed lock, a shorter execution time, a maxDuration that forces a stop, or a longer interval. A lock is the only one of those that is a guarantee.
The behaviours that surprise people
- A 404 still counts as a run. Vercel documents that a cron pointed at a nonexistent path generates a 404 but that the cron job still executes — that is, the request is still made. A renamed route produces a job that appears healthy in the settings pane and does nothing.
- Redirects are final. Cron invocations do not follow them. A 3xx completes the run. This catches trailing-slash and www-canonicalisation rules that are invisible in a browser.
- Rollbacks do not update crons. An Instant Rollback leaves active cron jobs unchanged; they continue on the old schedule until manually disabled or updated.
- Disabled crons still count. Vercel notes that disabled jobs remain listed and continue to count towards the per-project limit.
- Redirected and cached responses are missing from the logs. Vercel documents both, which makes “no log entry” an ambiguous signal rather than proof the job did not run.
- Multiple schedules can share one path. Read
x-vercel-cron-scheduleto tell them apart — the header contains the exact expression, so a single route can do a full sync on0 0 * * *and an incremental one on*/5 * * * *.
A last thing worth doing before you pick a frequency, because a cron spends money on a schedule and nobody is watching it. The invocation count is arithmetic:
*/1 * * * * 60 x 24 x 30 = 43,200 runs a month */5 * * * * 12 x 24 x 30 = 8,640 0 * * * * 24 x 30 = 720 0 6 * * * 30 = 30
Vercel is explicit that cron jobs invoke Vercel Functions and that the same usage and pricing applies, so those are function invocations before you count anything the model charges. A per-minute job that makes one model call is 43,200 completions a month whether or not there was work to do — which is the argument for the reconciliation query above doing its own cheap check first and returning early when the queue is empty, rather than calling a provider unconditionally on every tick.