# Vercel cron vs a workflow platform for scheduled jobs: which should I use?

> When Vercel Cron alone is enough, when to add Vercel Workflow or Inngest, and when a separate platform fits better. Includes current limits, retries, timezones and costs.

- Published: 2026-10-28
- URL: https://teton.so/guides/vercel-cron-vs-workflow-platform-for-scheduled-jobs

Use Vercel Cron by itself when the job is one short, idempotent request that can safely be missed or run twice. Vercel does not retry failed cron runs, and delivery is best effort. When the job has several steps, runs long, or must not lose work partway through, keep Vercel Cron as the trigger and run the work in a durable workflow. Vercel Workflow, Inngest and similar platforms retry the failed step instead of the whole job. If the job is a Claude Code workflow rather than part of your app, a platform that runs it outside your codebase, such as [Teton](https://teton.so), may be a better fit.

The sections below cover what each option does, its limits, and which jobs it suits.

## Option 1: Vercel Cron alone

Vercel Cron is a `crons` array in `vercel.json`. On schedule, Vercel sends a GET request to a route in your production deployment:

```json
{
  "crons": [
    { "path": "/api/cron/daily-sync", "schedule": "0 13 * * *" }
  ]
}
```

The route checks a shared secret and does the work. Set a `CRON_SECRET` environment variable of at least 16 random characters. Vercel sends it as a bearer token, and [Vercel's docs](https://vercel.com/docs/cron-jobs/manage-cron-jobs) show the handler rejecting anything else. Next.js reads the exported `GET` function from the route file:

```ts
// app/api/cron/daily-sync/route.ts
/* Next.js route handler */ export async function GET(request: Request) {
  const cronSecret = process.env.CRON_SECRET;
  const authHeader = request.headers.get('authorization');
  if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) {
    return new Response('Unauthorized', { status: 401 });
  }

  // Do the work here. Keep it idempotent.
  return Response.json({ ok: true });
}
```

To test locally, put `CRON_SECRET=local-dev-secret` in `.env.local` and call the route yourself, [as cronuru describes](https://cronuru.com/guides/vercel-cron):

```bash
curl http://localhost:3000/api/cron/daily-sync -H "Authorization: Bearer local-dev-secret"
```

### Current limits

Vercel changed these limits recently, and many blog posts still show the old ones. These figures come from [Vercel's usage and pricing page](https://vercel.com/docs/cron-jobs/usage-and-pricing) and [changelog](https://vercel.com/changelog/cron-jobs-now-support-100-per-project-on-every-plan) at the time of writing:

- **Jobs:** 100 per project on every plan. The old caps of 2 on Hobby and 20 per project no longer apply.
- **Frequency:** Hobby runs each job at most once a day, accurate only to the hour. A `0 1 * * *` job can fire any time between 1:00 and 1:59. A more frequent schedule fails the deploy. Pro and Enterprise can run every minute, accurate to the minute.
- **Timezone:** UTC only. A `0 13 * * *` job lands an hour off local time for part of the year in regions that use daylight saving.
- **Duration:** with Fluid compute, which has been the default for new projects since April 2025, functions get 300 seconds by default. Pro and Enterprise can raise `maxDuration` to 800 seconds ([HyperObserve](https://hyperobserve.com/limits/vercel/function-duration/)). Older projects may still have shorter limits.
- **Cost:** cron has no separate charge. Each run is billed as a normal function call.

### What it does not do

[Vercel's docs](https://vercel.com/docs/cron-jobs/manage-cron-jobs) are direct about this: "Vercel will not retry an invocation if a cron job fails." A run can be missed or delivered twice. If a job runs longer than its interval, a second copy can start while the first is still running. Vercel suggests a lock and recommends "reconciliation-based" jobs, where each run processes everything left over since the last successful run.

A few other behaviors cause silent failures ([Hyperping](https://hyperping.com/blog/how-to-monitor-vercel-cron-jobs)):

- Cron calls only the production deployment and does not follow redirects.
- The cron list is part of each deployment. If you merge a branch without the entry, the job disappears on the next production deploy.
- A `CRON_SECRET` set only for Preview, added after the last deploy, or pasted with a trailing newline makes every run return 401.

## Option 2: Vercel Cron that starts a Vercel Workflow

Vercel's own [background jobs guide](https://vercel.com/kb/guide/how-to-run-background-jobs-in-nextjs-on-vercel) recommends this setup. Cron stays the trigger, and the cron route calls `start()` on a durable workflow. Each step in the workflow is retried on its own, and `sleep()` waits without using compute.

```ts
// workflows/daily-sync.ts
/* started from the cron route */ export async function dailySync() {
  'use workflow';
  const items = await loadChanges();
  await sendSummary(items.length);
}

async function loadChanges() {
  'use step';
  const res = await fetch(process.env.SOURCE_URL!);
  if (!res.ok) throw new Error(`source returned ${res.status}`);
  return (await res.json()) as unknown[];
}

async function sendSummary(count: number) {
  'use step';
  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text: `${count} changes since last run` }),
  });
}
```

In the cron route, keep the secret check from Option 1 and replace the work with a call to `start()`:

```ts
// app/api/cron/daily-sync/route.ts (after the secret check above)
import { start } from 'workflow/api';
import { dailySync } from '@/workflows/daily-sync';

// inside GET:
await start(dailySync);
return Response.json({ started: true });
```

This setup has one gap. `start()` always generates its own run ID, and you [can't supply your own](https://github.com/vercel/workflow/issues/85). Because cron can fire twice, it can also start two workflows. Make the first step claim the run in a database and stop if the claim fails:

```sql
create table job_runs (job text, run_date date, primary key (job, run_date));
-- first step: insert ... on conflict do nothing; if no row was inserted, stop
insert into job_runs (job, run_date) values ('daily-sync', current_date)
on conflict do nothing;
```

Workflow has no built-in cron yet. A maintainer has also suggested starting a workflow once that loops on `sleep("1 day")` ([discussion #66](https://github.com/vercel/workflow/discussions/66)). How that loop behaves across redeploys isn't documented, so treat it as a stopgap. An [April 2026 RFC](https://github.com/vercel/workflow/discussions/1649) proposes a `schedule` property on workflows.

## Option 3: a third-party workflow platform such as Inngest

With Inngest, the schedule lives in your code and supports timezones. Inngest calls your app over HTTP and [retries only the failed step](https://www.inngest.com/uses/scheduled-jobs). Your code still runs on Vercel or wherever you host it. For a small app, the client, the function and the handler can share one route file:

```ts
// app/api/inngest/route.ts
import { Inngest } from 'inngest';
import { serve } from 'inngest/next';

const inngest = new Inngest({ id: 'my-app' });

const weekdaySync = inngest.createFunction(
  { id: 'weekday-sync', retries: 2, triggers: [{ cron: 'TZ=America/New_York 0 8 * * 1-5' }] },
  async ({ step }) => {
    const items = await step.run('load', async () => {
      const res = await fetch(process.env.SOURCE_URL!);
      return (await res.json()) as unknown[];
    });
    await step.run('notify', async () => ({ count: items.length }));
  }
);

/* Inngest calls these handlers */ export const { GET, POST, PUT } = serve({ client: inngest, functions: [weekdaySync] });
```

You need an `INNGEST_SIGNING_KEY` and must sync the app with Inngest. The cost is another SDK, another account, and rewriting jobs as Inngest functions. That is [more than you need](https://tickstem.dev/blog/vercel-cron-alternative) if you only call one endpoint. Published Inngest pricing figures disagree, so check Inngest's pricing page directly. Trigger.dev is a similar code-first option. Compare its schedule and timezone docs before choosing.

## Option 4: an external scheduler calling your route

Services like cron-job.org, GitHub Actions schedules or Upstash can call your route instead of Vercel. Teams usually do this to get past Hobby's once-a-day limit or to get timezone support. You still have a single request with no step retries, so the same idempotency rules apply. Whichever scheduler you use, add a heartbeat monitor that your job pings only after the work succeeds. Hyperping notes that Vercel's logs alone don't prove a job finished. Sentry's Next.js SDK [creates monitors automatically](https://sentry.io/changelog/2023-5-13-automatic-monitors-for-vercel-cron-jobs) for crons in `vercel.json`.

## Option 5: Teton, for Claude Code jobs outside your app

Some scheduled jobs aren't part of your product at all: a morning HubSpot pipeline summary in Slack, or flagging deals that have gone quiet. For these, putting a route in a Next.js app adds a codebase to maintain. [Teton](https://teton.so) runs Claude Code workflows headless in the cloud. You do the job once with Claude, type `/teton`, and Claude writes it up as one workflow file with the trigger, the steps and the credentials it needs. One deploy makes it live.

Teton covers several of Vercel Cron's gaps:

- Schedules are cron with a timezone. A workflow can also start from a webhook or by hand.
- Every run is listed with its result and cost.
- Each workspace has a Postgres database. Jobs can record where they stopped, which supports the reconciliation pattern Vercel recommends.
- Every version is kept, and making the last good version live again takes one click.

It is not the right tool for jobs that belong in your application code, such as cleaning up your own app's tables. For those, use one of the options above.

## Watch the side effects, not just the cron bill

Cron runs are cheap, but what they touch may not be. [One team](https://github.com/totalolage/deskohub-site/issues/741) estimated about $1.34 a month in Vercel charges for a 3-second job running every minute. Each run woke their Neon database and kept it awake around the clock, which cost about $19 a month. They moved the job to every 15 minutes. Before choosing a frequency, check what each run wakes up.

## How to choose

| | Where the schedule lives | Timezone | When a run fails | Best for |
| --- | --- | --- | --- | --- |
| Vercel Cron alone | `vercel.json`, per deploy | UTC only | Not retried | Short, idempotent single requests |
| Vercel Cron + Workflow | `vercel.json` + workflow code | UTC only | Failed step retried | Multi-step or long jobs in a Vercel app |
| Inngest | Your function code | `TZ=` supported | Failed step retried | Multi-step jobs, local-time schedules |
| External scheduler | The scheduler's dashboard | Depends on service | Depends on service | Getting past Hobby limits |
| Teton | One workflow file | Supported | Listed with result and cost | Claude Code GTM jobs outside your app |

Start with Vercel Cron alone. Add Workflow or Inngest when a partial failure would cost you something, or when the job outgrows a single function call.

## FAQ

### Can I run Vercel Cron more than once a day on Hobby?

No. Hobby schedules that would run more than once a day fail at deploy. Upgrade to Pro, or have an external scheduler call the route.

### Does Vercel retry a failed cron job?

No. Retry inside the job, start a durable workflow that retries steps, or write each run so it catches up on everything left over since the last success.

### Why did my cron job stop running?

Check that the entry is still in the deployed `vercel.json`. Check that `CRON_SECRET` is set for Production, was set before the last deploy, and has no trailing newline. Also confirm the route doesn't redirect.

### Do I need a workflow platform for a daily report?

Usually not. A daily report that can safely re-run works fine on Vercel Cron alone. Add a workflow when the job has steps that must not repeat, such as sending messages or writing to a CRM.

---

- [All guides](https://teton.so/guides)
- [Teton](https://teton.so): the cloud for Claude Code. Build a workflow with Claude once and Teton runs it on a schedule or a webhook.
