Cron Jobs in Next.js with Vercel Cron — Scheduled Functions
Run scheduled tasks in Next.js using Vercel Cron (serverless cron), GitHub Actions schedules, or Upstash QStash. Includes configuration, securing cron endpoints, and...
Next.js doesn’t have built-in cron scheduling, but Vercel Cron, GitHub Actions, and Upstash QStash all fill this gap. Here’s how to implement scheduled tasks for different deployment scenarios.
Use the Cron Builder to generate cron expressions for your schedule.
Vercel Cron (Vercel deployment)
Vercel Cron calls API routes on a schedule defined in vercel.json:
// vercel.json
{
"crons": [
{
"path": "/api/cron/daily-report",
"schedule": "0 8 * * *"
},
{
"path": "/api/cron/cleanup",
"schedule": "0 2 * * 0"
}
]
}
// app/api/cron/daily-report/route.ts (Next.js 13+ App Router)
import { NextRequest, NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
export const maxDuration = 60; // 60 second timeout
export async function GET(req: NextRequest) {
// Verify it's actually Vercel calling (not a random HTTP request):
const authHeader = req.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Your cron logic:
const report = await generateDailyReport();
await sendEmail(report);
return NextResponse.json({ ok: true, processedAt: new Date().toISOString() });
}
# .env.local:
CRON_SECRET=your-random-secret-here
# Vercel automatically sends:
# Authorization: Bearer <CRON_SECRET>
# x-vercel-cron: 1
Secure your cron endpoint
Without security, anyone can trigger your cron endpoint:
// pages/api/cron/[task].ts (Pages Router)
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Method check:
if (req.method !== 'GET') {
return res.status(405).end();
}
// Auth check:
if (req.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Rate limit: prevent rapid re-triggering
const key = `cron:${req.query.task}:${Math.floor(Date.now() / 55000)}`; // 55s window
const existing = await redis.get(key);
if (existing) {
return res.status(429).json({ error: 'Already running' });
}
await redis.setex(key, 60, '1');
try {
await runTask(req.query.task as string);
return res.json({ ok: true });
} catch (err) {
console.error('Cron failed:', err);
return res.status(500).json({ error: 'Cron failed' });
}
}
GitHub Actions as a cron scheduler
For any deployment (Vercel, Netlify, Railway, self-hosted):
# .github/workflows/daily-tasks.yml
name: Daily Tasks
on:
schedule:
- cron: '0 8 * * *' # 8 AM UTC daily
workflow_dispatch: # Allow manual trigger
jobs:
run-cron:
runs-on: ubuntu-latest
steps:
- name: Trigger daily report
run: |
curl -s -X GET \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
"${{ vars.APP_URL }}/api/cron/daily-report"
Advantages over Vercel Cron:
- Works with any hosting
- Runs in GitHub’s infrastructure (not your server quota)
- Easy logging and history
- Can run multiple tasks in parallel
Upstash QStash for Next.js
QStash is a serverless message queue that supports cron:
// app/api/cron/weekly/route.ts
import { verifySignatureAppRouter } from '@upstash/qstash/dist/nextjs';
async function handler(req: Request) {
// Generate weekly digest email
const users = await db.users.findMany({ where: { emailDigest: true } });
for (const user of users) {
await sendDigestEmail(user);
}
return Response.json({ sent: users.length });
}
export const POST = verifySignatureAppRouter(handler);
// Schedule the cron via QStash API (once, in setup):
import { Client } from '@upstash/qstash';
const client = new Client({ token: process.env.QSTASH_TOKEN });
await client.schedules.create({
destination: 'https://yourapp.com/api/cron/weekly',
cron: '0 9 * * 1', // 9 AM Monday
});
Self-hosted: node-cron
For self-hosted Node.js deployments:
// cron-runner.ts
import cron from 'node-cron'; // npm install node-cron
// Daily at 8 AM:
cron.schedule('0 8 * * *', async () => {
console.log('[CRON] Running daily report');
try {
await generateDailyReport();
} catch (err) {
console.error('[CRON] Daily report failed:', err);
}
}, { timezone: 'America/New_York' });
// Every 15 minutes:
cron.schedule('*/15 * * * *', async () => {
await processQueuedEmails();
});
Note: node-cron only works with long-running processes — not serverless functions which shut down between requests.
Related tools
- Cron Builder — generate cron expressions
- Cron Expression Guide — cron syntax reference
- Cron Job Scheduling Guide — cron daemon and best practices
Related posts
- Cron Expression Syntax, Explained by Field — Five fields, nine special characters, a dozen edge cases. A complete reference f…
- Cron Pitfalls: Timezones, DST, and Missed Runs — Your cron job will run twice or skip entirely when DST changes. Here's why, how …
- AWS EventBridge Scheduler — Cron Schedules for Lambda and AWS Services — AWS EventBridge Scheduler runs Lambda functions, ECS tasks, and other AWS target…
- Cron Expression Guide — Syntax, Fields, and Special Characters — Cron expressions schedule recurring jobs using five or six fields: minute, hour,…
- Cron Job Scheduling — How Cron Works and Best Practices — Cron runs scheduled tasks using 5-field time expressions. Learn how cron daemons…
Related tool
Build and parse cron expressions with human-readable explanations.
Written by Mian Ali Khalid. Part of the Dev Productivity pillar.