Skip to content

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.

From Vercel’s “Usage & Pricing for Cron Jobs”, read 11 August 2026. Plan-gated limits are the fastest-moving figures in this territory.

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

  1. Add the schedule. The crons array in vercel.json takes path and schedule:
    {
      "$schema": "https://openapi.vercel.sh/vercel.json",
      "crons": [
        { "path": "/api/cron/digest", "schedule": "0 6 * * *" }
      ]
    }
  2. Set CRON_SECRET. Vercel documents that adding an environment variable of that name causes its value to be sent automatically as an Authorization header on cron invocations, with a Bearer prefix, and recommends a random string of at least 16 characters:
    openssl rand -base64 32 | vercel env add CRON_SECRET production
  3. 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 });
    }
  4. Deploy to production. The schedule is created from the deployment, so a cron in vercel.json on 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.
  5. Test by calling the endpoint directly. Vercel documents that there is no support for cron in vercel dev, next dev or other framework dev servers — a cron job is just a route, so exercise it with curl and 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-schedule to tell them apart — the header contains the exact expression, so a single route can do a full sync on 0 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.