Cron Jobs
Schedule recurring tasks on Vercel with cron jobs defined in vercel.json — syntax, securing them, plan limits, and gotchas.
Cron jobs run a function on a schedule — nightly backups, periodic syncs, scheduled emails. On Vercel you define them in vercel.json, and at each scheduled time Vercel sends an HTTP GET request to the path you specify on your production deployment.
Defining a cron job
Add a crons array to vercel.json, where each entry has a path (the route to invoke) and a schedule (a standard 5-field Unix cron expression):
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"crons": [
{ "path": "/api/cron/daily-backup", "schedule": "0 2 * * *" },
{ "path": "/api/cron/sync", "schedule": "*/15 * * * *" }
]
}The matching route is just a normal API route / Route Handler:
// app/api/cron/daily-backup/route.ts
export async function GET(request: Request) {
// ... do the scheduled work
return Response.json({ ok: true });
}When you deploy, Vercel registers the schedule automatically. Cron jobs run only for production deployments, not previews.
Securing the endpoint
The cron path is a public HTTP endpoint — anyone who knows the URL could hit it. Vercel auto-provisions a CRON_SECRET environment variable and sends it as an Authorization: Bearer header when it invokes your job. Verify it before doing real work:
export async function GET(request: Request) {
const auth = request.headers.get('authorization');
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
// ... safe to proceed
return Response.json({ ok: true });
}Things to design for
Cron delivery is best-effort, so build your jobs to tolerate it:
- No retries. If your function errors, Vercel does not retry — it just logs a failure. Add your own retry/reconciliation logic and check the logs via the Cron Jobs section in the dashboard.
- Possible duplicate or missed runs. A run can occasionally fire more than once. Make operations idempotent — "set status to active" is safe to run twice; "increment credit by 10" is not.
- No overlap protection. If a job runs every minute but sometimes takes three, invocations can overlap. Use a lock if that matters.
- GET only. Cron always triggers a
GET. If your logic lives in aPOSTendpoint, wrap it in aGETroute. - UTC only. Schedules are evaluated in UTC; convert local times yourself.
- Same duration limits as functions. A cron invocation is a normal function call, so the function timeout applies. For long work, split it up or kick off a background job and return quickly.
Plan limits
Cron jobs are available on all plans, and you can define up to 100 per project on every plan (per-team limits were removed in early 2026). The main free-tier constraint is cadence: the Hobby plan only accepts schedules that fire at most once per day, while Pro and Enterprise allow minute-level schedules. If you need sub-daily runs on Hobby, point an external scheduler at your (secured) route instead. Exact limits shift over time — confirm current values on vercel.com and see Pricing & plans.
Testing locally
A cron route is just an API route, so hit it directly in development:
curl http://localhost:3000/api/cron/daily-backupNote that production cron invocations do not follow redirects, even though your browser might.