Guide··Last updated

Vercel Cron Jobs: Limits by Plan, Setup, and Why They Fail

How Vercel cron jobs work: the vercel.json setup, the once-a-day Hobby limit, UTC only, no retries, and what to do when a run is missed.

Vercel cron jobs call a route in your production deployment on a schedule you set in vercel.json, and on the Hobby plan they can run at most once a day, somewhere inside the hour you picked. The schedule is always UTC, a failed run isn't retried, and anything more often than daily needs Pro or Enterprise.

How Vercel cron jobs work

You add cron jobs in vercel.json, under crons. Each entry has a path and a schedule:

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "crons": [
    {
      "path": "/api/cron",
      "schedule": "0 9 * * *"
    }
  ]
}

That one runs every day at 09:00 UTC. You can also add cron jobs through the Build Output API.

When the schedule fires, Vercel makes an HTTP GET request to your project's production deployment URL with that path. The user agent is always vercel-cron/1.0, and each request has an x-vercel-cron-schedule header with the expression that triggered it, which helps when several crons share one path. The timezone is always UTC (Vercel's cron jobs docs).

Anyone who knows the URL can call that route too, so protect it. Set an environment variable called CRON_SECRET (Vercel recommends a random string of at least 16 characters), and Vercel sends it as Authorization: Bearer <CRON_SECRET> on every cron request. Here's the handler from Vercel's page on managing cron jobs:

import type { NextRequest } from 'next/server';

export function GET(request: NextRequest) {
  const authHeader = request.headers.get('authorization');
  const cronSecret = process.env.CRON_SECRET;

  if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) {
    return new Response('Unauthorized', { status: 401 });
  }

  return Response.json({ success: true });
}

Next.js cron jobs on Vercel

In Next.js, a cron is a route handler. Put the code above in app/api/cron/route.ts and point the path at /api/cron. I'd also add export const dynamic = 'force-dynamic'; to that file, so the response isn't cached. The duration limits are the same as any Vercel Function, so set maxDuration if the job is slow.

Vercel cron limits by plan

PlanCron jobs per projectMinimum intervalPrecision
Hobby100Once per dayPer hour (±59 min)
Pro100Once per minutePer minute
Enterprise100Once per minutePer minute

As of October 29, 2026, from Vercel's usage and pricing page.

The Hobby precision is easy to miss. A schedule of 0 8 * * * can fire any time between 08:00:00 and 08:59:59, because Vercel spreads the load across the hour. On Pro and Enterprise, 5 8 * * * fires between 08:05:00 and 08:05:59.

That's the Vercel cron Hobby limit in practice. Anything that would run more than once a day, like 0 * * * * or */30 * * * *, fails the deployment with this error:

Hobby accounts are limited to daily cron jobs. This cron expression would run more than once per day.

Cron jobs are included in every plan. The functions they call are billed as normal Vercel Functions usage.

Why your Vercel cron didn't run

When a Vercel cron is not running, it's usually one of the causes below, from Vercel's troubleshooting guide and cron management docs. Start with View Logs in the project's Cron Jobs settings. To trigger the deployed job without waiting, the Vercel CLI has vercel crons run (in beta):

vercel crons run /api/cron
  • It's on Hobby and ran later in the hour than you expected. Anything inside that hour counts as on time. If the minute matters, you need Pro.
  • The hours are UTC. 0 9 * * * is 09:00 UTC, so convert from your local time before you write the expression.
  • The run failed and nothing retried it. Vercel doesn't retry a failed cron invocation. Check the logs, and write the job so the next run picks up what this one missed.
  • Delivery is best effort. A transient network error can stop the request reaching your function, and then it doesn't run and leaves no runtime log. The same run can also occasionally fire twice. Make the job idempotent and reconciliation based, so each run processes everything since the last successful one.
  • Two runs overlapped. If a run takes longer than the interval, Vercel can start a second one while the first is still going. Use a lock (Redis works), a shorter job, maxDuration, or a longer interval.
  • A redirect ended it. Cron jobs don't follow redirects, so a 3xx ends the invocation. With trailingSlash enabled, add the trailing slash to the cron path. Auth middleware that redirects to a login page does the same, so let the cron path through.
  • The response was cached. Redirects and cached responses don't show in the logs, so a cached route looks like a cron that never ran. Add export const dynamic = 'force-dynamic'; to the route handler.
  • You're checking a Preview deployment. Cron jobs only run on Production deployments.
  • A firewall rule blocked it. Custom WAF rules can block cron requests, so check yours for the cron path.
  • The secret is malformed. A CRON_SECRET with newlines or characters that aren't valid in an Authorization header breaks the check, so your handler returns a 401 on every run.

Check the expression first

Vercel's expressions have a few rules of their own. Day of week is 0 to 6 (Sunday to Saturday), and names like MON, SUN, JAN or DEC aren't supported. You can't set both day of month and day of week, so when one has a value the other has to be *. And the timezone is always UTC.

Before you deploy, check the expression and its next UTC run times in the generator. Changing a schedule later means editing vercel.json and redeploying, so I'd check it before the first push.

When to call the URL from outside Vercel

Three things push people outside Vercel cron: more than one run a day on Hobby, retries, and an email when a run fails. Pro gives you once a minute with per-minute precision, though a failed run still isn't retried. The other option is to keep the route on Vercel and have a hosted scheduler call it.

That second one is what we built Omniyond for, so here are its limits first. It takes an interval written 5m, 1h or 1d, from 5 minutes up to 90 days. It doesn't take cron expressions or time zones, and it can't run anything more often than every 5 minutes.

It calls any public HTTP or HTTPS URL with the method, headers and body you set. Custom headers work, so it can call the same protected route with your CRON_SECRET. The default method is POST, so set "method": "GET" for a Vercel cron route:

curl -X POST https://omniyond.com/v1/schedule \
  -H "Authorization: Bearer omni_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.vercel.app/api/cron",
    "method": "GET",
    "headers": { "Authorization": "Bearer your_cron_secret" },
    "schedule": "1h",
    "maxRuns": 720
  }'

With an API key, each run is prepaid from your credits, and maxRuns sets how many runs you prepay, up to 1000. Leave it out and the job runs once. You create jobs with the API like above, or an agent creates them over MCP with the schedule_job tool. That last one matters when an agent sets the schedule, since it can't edit your vercel.json.

The first run is when you create the job, or at a runAt ISO time you pass. After each run, the next one is set one interval after it finishes, and a once-a-minute check picks up due jobs. So the interval counts from the end of the last run, not from the clock, and runs of one job don't overlap.

If the call errors, times out after 30 seconds, or returns a 5xx or a 429, it's retried after 30 seconds, then 5 minutes, then 30 minutes. If it still fails, the job is marked failed and your project admins get an email with the error. It stays failed until you restart it from the dashboard, the API or the restart_job MCP tool.

It follows up to 5 redirects, which Vercel cron doesn't. Every run is recorded, and results are stored to fetch later or POSTed to a callback URL.

One gotcha: other 4xx responses aren't retried. A 401 from a wrong secret is recorded as a successful run with HTTP 401 and uses one of your prepaid runs, so check the response code on the first run.

If you're comparing services, here's a hosted cron with retries next to EasyCron.

On Hobby and need it more than once a day, with retries and an email when it fails? Call your Vercel route from Omniyond

FAQ

Are Vercel cron jobs free?

Cron jobs are included in all plans, Hobby too. The functions they call are billed as normal Vercel Functions usage.

How often can a cron job run on Vercel Hobby?

Once a day at most, with per-hour precision, so 0 8 * * * runs some time between 08:00 and 08:59 UTC. Pro and Enterprise go down to once a minute.

Why does my deploy fail with "Hobby accounts are limited to daily cron jobs"?

The expression would run more than once a day, like 0 * * * * or */30 * * * *. Change it to a daily schedule, or move the project to Pro.

What timezone do Vercel cron jobs use?

UTC, always. There's no timezone setting, so convert your local time to UTC when you write the expression.

Does Vercel retry a failed cron job?

No. A failed invocation isn't retried. Check View Logs in the project's Cron Jobs settings, and write the job so the next run catches up on what the failed one missed.

How do I test a Vercel cron job locally?

Call the route yourself, for example http://localhost:3000/api/cron, with the Authorization header if you check CRON_SECRET. vercel dev, next dev and other framework dev servers don't run cron jobs. For the deployed job, vercel crons run /api/cron (beta) triggers it without waiting for the schedule.

How do I secure a Vercel cron route?

Set a CRON_SECRET environment variable to a random string of at least 16 characters. Vercel sends it as Authorization: Bearer <CRON_SECRET>, and your handler returns a 401 when the header doesn't match.

Can I run a job every 5 minutes on Vercel Hobby?

No, Hobby is limited to once a day. On Pro, */5 * * * * works, and that's the cron expression for every 5 minutes on most schedulers. On Hobby, you can call the route from an external scheduler on a 5m interval.

Vercel details checked on October 29, 2026 on the docs linked above. They change, so check the source before you rely on them.

Omniyond is an API that schedules any HTTP call. You give it a URL and a cadence, and it calls that URL once or on a repeat, retries if the call fails, and stores the result or POSTs it to your callback. Each run costs $0.01 from prepaid credits, and there's no subscription. How Omniyond works