How to Run Cron Jobs in Next.js (With and Without Vercel)
Next.js has no built-in scheduler. Here are the three practical ways to run scheduled tasks in a Next.js app — Vercel Cron, an external scheduler, and node-cron — with code and trade-offs.
You built your app in Next.js, and now you need something to run on a schedule: send digest emails, clean up expired sessions, sync data from an external API, regenerate a sitemap.
Then you hit the wall every Next.js developer hits: Next.js has no scheduler. There's no next cron command. Serverless functions only run when something calls them.
The good news: the fix is simple. You expose the task as an API route, and something else calls that route on a schedule. The only real question is what that "something else" is. Let's go through the three options.
Step 1: Write the task as an API route
Whatever scheduler you pick, the task itself looks the same — a route handler that does the work:
// app/api/cron/cleanup/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
// 1. Reject anyone who isn't your scheduler (see below)
const auth = request.headers.get("authorization");
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 2. Do the actual work
const deleted = await prisma.session.deleteMany({
where: { expiresAt: { lt: new Date() } },
});
return NextResponse.json({ ok: true, deleted: deleted.count });
}
Two details matter here:
- Protect the route. A cron endpoint that anyone can trigger is an invitation to abuse. Check a secret header and store the secret in an environment variable.
- Return real status codes. Return
500when the work fails, not a200with an error in the body. Your scheduler decides success or failure from the HTTP status — a job that "fails successfully" will never trigger an alert.
Option A: Vercel Cron
If you deploy on Vercel, you can declare schedules in vercel.json:
{
"crons": [
{ "path": "/api/cron/cleanup", "schedule": "0 3 * * *" }
]
}
It's the zero-setup option, but know the limits before you rely on it:
- On the Hobby (free) plan you get 2 cron jobs, and they can run at most once per day — and not at a precise time, but within an hour-long window of the scheduled time.
- Schedules only run in production, and every change requires a redeploy.
- There's no execution history, no failure alerting, and no retries. If your job throws, nothing tells you.
That last point is the deal-breaker for anything important. Vercel Cron triggers your job; it doesn't monitor it.
Option B: An external scheduler (works anywhere)
The second option decouples scheduling from hosting: keep the API route, and let a dedicated cron service call it. This works identically on Vercel, Netlify, Railway, a VPS — anywhere your app has a URL.
With CronSpark the setup is: create a job, paste https://yourapp.com/api/cron/cleanup, pick a schedule (a cron expression or an interval down to 10 seconds), and add your Authorization header. Secrets are stored in an encrypted vault and injected into the header at request time, so they never sit in a dashboard field in plain text.
What you get over a platform-native trigger:
- Failure alerts on email, Discord, Slack or Telegram the moment a run returns a non-2xx, times out, or the response body doesn't match a pattern you expect.
- Execution logs — status code, duration, and response body for every run, so 3 AM failures are debuggable at 9 AM.
- Retries with backoff, precise timing, per-job timezones, and no redeploy to change a schedule.
- No plan-based schedule limits tied to your hosting provider.
This is the setup we recommend for production apps: your code stays in your repo, your scheduling and monitoring live in one place outside your infrastructure — so it still works (and still alerts you) when your app is the thing that's down.
Option C: node-cron (only for long-running servers)
If you run Next.js as a persistent Node process on a VPS (next start behind nginx, or a custom server), you can schedule inside the process:
import cron from "node-cron";
cron.schedule("0 3 * * *", async () => {
await cleanupExpiredSessions();
});
We covered this approach in depth in how to set up cron jobs in Node.js, but be aware of the failure modes:
- It does not work on serverless. On Vercel or Netlify there is no long-lived process to hold the schedule.
- Multiple instances = duplicate runs. Two replicas behind a load balancer will both fire the job.
- A restart at the wrong moment silently skips a run — and nothing tells you.
If you go this route, at least add a heartbeat: ping a dead man's switch monitor at the end of each run, so a missed run becomes an alert instead of a mystery.
Which one should you pick?
- Hobby project on Vercel, one daily task, failure is annoying but harmless → Vercel Cron is fine.
- Anything users or revenue depend on → external scheduler with alerting (CronSpark has a free tier with 5 jobs and email alerts).
- Single persistent VPS process → node-cron works, but wrap it with heartbeat monitoring.
The pattern to remember: in Next.js, scheduled work is just an HTTP endpoint plus something reliable that calls it — and "reliable" means it tells you when the call fails.