← Back to articles

How to Add Cron Jobs to Next.js (2026 Guide)

Need to send daily emails, sync data hourly, or clean up expired records? Next.js doesn't include cron jobs out of the box, but several solutions make it easy.

Your Options

SolutionBest ForCostComplexity
Vercel CronSimple scheduled API callsFree (Vercel plan)Lowest
Upstash QStashServerless-friendly schedulingFree tierLow
InngestEvent-driven + scheduled workflowsFree tierMedium
Trigger.devLong-running scheduled jobsFree tierMedium
node-cron (self-hosted)Custom Node.js serverFreeLow (if self-hosted)

Option 1: Vercel Cron Jobs (Simplest)

If you're on Vercel, this is the fastest path. Define a cron schedule in vercel.json and point it at an API route.

Setup

1. Create an API route:

// app/api/cron/daily-cleanup/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  // Verify the request is from Vercel Cron
  const authHeader = request.headers.get('authorization');
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Your scheduled task
  await db.expiredSessions.deleteMany({
    where: { expiresAt: { lt: new Date() } }
  });

  return NextResponse.json({ success: true });
}

2. Configure the schedule:

// vercel.json
{
  "crons": [
    {
      "path": "/api/cron/daily-cleanup",
      "schedule": "0 3 * * *"
    }
  ]
}

3. Add a CRON_SECRET to your environment variables for security.

Limitations

  • Max execution time: 10s (Hobby), 60s (Pro), 900s (Enterprise)
  • No retry logic built-in
  • No job queuing — just HTTP calls on a schedule
  • Vercel-only

Best For

Simple periodic tasks: database cleanup, sending daily digests, syncing data from external APIs.

Option 2: Upstash QStash (Serverless-Friendly)

QStash is a message queue that can schedule HTTP calls. Works with any serverless platform.

Setup

// Schedule a recurring job
import { Client } from '@upstash/qstash';

const qstash = new Client({ token: process.env.QSTASH_TOKEN! });

// Create a schedule
await qstash.schedules.create({
  destination: 'https://your-app.vercel.app/api/cron/sync',
  cron: '*/30 * * * *', // Every 30 minutes
});
// app/api/cron/sync/route.ts
import { verifySignatureAppRouter } from '@upstash/qstash/nextjs';

async function handler(request: Request) {
  // QStash handles authentication via signature verification
  await syncExternalData();
  return new Response('OK');
}

// Wraps handler with signature verification
export const POST = verifySignatureAppRouter(handler);

Strengths

  • Works with any hosting provider (not just Vercel)
  • Built-in retries (configurable)
  • Message deduplication
  • Delay scheduling (run once in 5 minutes)
  • Generous free tier (500 messages/day)

Best For

Serverless deployments where you need reliable scheduling with retries across any platform.

Option 3: Inngest (Event-Driven Scheduling)

Inngest combines scheduled tasks with event-driven workflows. It's more powerful than simple cron.

Setup

// inngest/functions.ts
import { inngest } from './client';

export const weeklyReport = inngest.createFunction(
  { id: 'weekly-report', name: 'Send Weekly Report' },
  { cron: '0 9 * * 1' }, // Every Monday at 9 AM
  async ({ step }) => {
    const data = await step.run('fetch-metrics', async () => {
      return await getWeeklyMetrics();
    });

    await step.run('generate-report', async () => {
      return await generateReport(data);
    });

    await step.run('send-email', async () => {
      await sendEmail({
        to: 'team@company.com',
        subject: 'Weekly Report',
        html: renderReport(data),
      });
    });
  }
);
// app/api/inngest/route.ts
import { serve } from 'inngest/next';
import { inngest } from '@/inngest/client';
import { weeklyReport } from '@/inngest/functions';

export const { GET, POST, PUT } = serve({
  client: inngest,
  functions: [weeklyReport],
});

Strengths

  • Step functions: Break jobs into retryable steps. If step 3 fails, it retries from step 3 (not from scratch).
  • Event-driven + scheduled: Combine cron triggers with event triggers in the same system.
  • Built-in observability: Dashboard shows every function run with logs and traces.
  • Automatic retries: Configurable retry policies per function.
  • Concurrency control: Limit how many instances run simultaneously.

Best For

Complex workflows that need step-by-step reliability, event + schedule triggers, and observability.

Option 4: Trigger.dev (Long-Running Jobs)

Trigger.dev excels at scheduled jobs that run for minutes or hours.

Setup

// trigger/scheduled.ts
import { schedules } from '@trigger.dev/sdk/v3';

export const dailySync = schedules.task({
  id: 'daily-data-sync',
  cron: '0 2 * * *', // 2 AM daily
  run: async (payload) => {
    // Can run for minutes/hours
    const users = await db.user.findMany();
    
    for (const user of users) {
      await syncUserData(user);
      // Trigger.dev handles checkpointing
    }

    return { synced: users.length };
  },
});

Strengths

  • Long-running jobs (minutes to hours)
  • No timeout limitations
  • Built-in dashboard
  • Self-hostable

Best For

Data-intensive scheduled jobs that exceed serverless time limits.

Option 5: Self-Hosted node-cron

If you're running Next.js on your own server (not serverless), use node-cron directly.

Setup

// lib/cron.ts
import cron from 'node-cron';

// Only run in production, only on one instance
if (process.env.NODE_ENV === 'production') {
  cron.schedule('0 * * * *', async () => {
    console.log('Running hourly task');
    await cleanupExpiredTokens();
  });

  cron.schedule('0 9 * * *', async () => {
    console.log('Running daily report');
    await sendDailyReport();
  });
}

Limitations

  • Only works on persistent servers (not serverless)
  • No built-in retries or observability
  • Must handle multiple instances yourself (avoid duplicate runs)
  • No dashboard

Best For

Self-hosted Next.js on VPS/Docker where you don't want external dependencies.

Comparison

FeatureVercel CronQStashInngestTrigger.devnode-cron
Max duration10-900s15min15minHoursUnlimited
RetriesNoYesYesYesDIY
DashboardVercel logsQStash UIYesYesNo
Step functionsNoNoYesYesNo
Self-hostNoNoYesYesYes
Free tierWith Vercel500/day25K runs50K runsFree

Which Should You Choose?

Simple cleanup tasks, daily emails: → Vercel Cron (if on Vercel) or QStash

Multi-step workflows with retries: → Inngest

Long-running data processing: → Trigger.dev

Self-hosted, no external deps: → node-cron

Platform-agnostic with retries: → QStash

FAQ

Can I use multiple solutions together?

Yes. Many teams use Vercel Cron for simple tasks and Inngest for complex workflows in the same app.

How do I prevent duplicate cron runs?

Vercel Cron and QStash handle this automatically. For node-cron with multiple instances, use a distributed lock (Redis lock or database advisory lock).

What about timezone handling?

Vercel Cron runs in UTC. QStash supports timezone configuration. Inngest and Trigger.dev support timezone-aware scheduling.

How do I test cron jobs locally?

Most solutions provide CLI tools: inngest dev, trigger.dev dev, npx qstash. For Vercel Cron, just call the API route directly during development.

The Bottom Line

Start with Vercel Cron if you're on Vercel and need simple scheduling. Graduate to Inngest when you need reliability, retries, and step functions. Use Trigger.dev for long-running scheduled tasks that exceed serverless limits.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.