X Xerobit

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...

Mian Ali Khalid · · 5 min read
Use the tool
Cron Builder
Build and parse cron expressions with human-readable explanations.
Open Cron Builder →

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 posts

Related tool

Cron Builder

Build and parse cron expressions with human-readable explanations.

Written by Mian Ali Khalid. Part of the Dev Productivity pillar.