Skip to main content
Back to Blog
Tutorials
2 min read
November 11, 2024

How to Implement Cron Jobs with Vercel and Next.js

Set up scheduled tasks in Next.js using Vercel Cron Jobs for report generation, cleanup, and automated workflows.

Ryel Banfield

Founder & Lead Developer

Cron jobs automate recurring tasks like sending digest emails, cleaning up old data, or generating reports.

Step 1: Configure vercel.json

{
  "crons": [
    {
      "path": "/api/cron/daily-digest",
      "schedule": "0 9 * * *"
    },
    {
      "path": "/api/cron/cleanup",
      "schedule": "0 0 * * 0"
    },
    {
      "path": "/api/cron/weekly-report",
      "schedule": "0 8 * * 1"
    }
  ]
}

Common cron schedules:

  • 0 9 * * * — Every day at 9:00 AM UTC
  • 0 */6 * * * — Every 6 hours
  • 0 0 * * 0 — Every Sunday at midnight
  • 0 0 1 * * — First day of every month

Step 2: Secure Your Cron Endpoints

// lib/cron.ts
import { NextResponse } from "next/server";

export function verifyCronSecret(req: Request) {
  const authHeader = req.headers.get("authorization");
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  return null;
}

Step 3: Daily Digest Email

// app/api/cron/daily-digest/route.ts
import { NextResponse } from "next/server";
import { verifyCronSecret } from "@/lib/cron";
import { db } from "@/db";
import { users, activities } from "@/db/schema";
import { Resend } from "resend";
import { gte } from "drizzle-orm";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function GET(req: Request) {
  const error = verifyCronSecret(req);
  if (error) return error;

  const yesterday = new Date(Date.now() - 86400000);

  // Get users who opted in to daily digests
  const subscribers = await db
    .select()
    .from(users)
    .where(gte(users.dailyDigest, true));

  for (const user of subscribers) {
    const recentActivities = await db
      .select()
      .from(activities)
      .where(gte(activities.createdAt, yesterday));

    if (recentActivities.length === 0) continue;

    await resend.emails.send({
      from: "digest@yourdomain.com",
      to: user.email,
      subject: `Daily Digest — ${recentActivities.length} updates`,
      html: `
        <h2>Your Daily Digest</h2>
        <ul>
          ${recentActivities
            .map((a) => `<li>${a.description}</li>`)
            .join("")}
        </ul>
      `,
    });
  }

  return NextResponse.json({
    success: true,
    emailsSent: subscribers.length,
  });
}

Step 4: Data Cleanup Job

// app/api/cron/cleanup/route.ts
import { NextResponse } from "next/server";
import { verifyCronSecret } from "@/lib/cron";
import { db } from "@/db";
import { sessions, logs, tempFiles } from "@/db/schema";
import { lt } from "drizzle-orm";

export async function GET(req: Request) {
  const error = verifyCronSecret(req);
  if (error) return error;

  const thirtyDaysAgo = new Date(Date.now() - 30 * 86400000);
  const sevenDaysAgo = new Date(Date.now() - 7 * 86400000);

  // Delete expired sessions
  const deletedSessions = await db
    .delete(sessions)
    .where(lt(sessions.expiresAt, new Date()));

  // Delete old logs
  const deletedLogs = await db
    .delete(logs)
    .where(lt(logs.createdAt, thirtyDaysAgo));

  // Delete temp files
  const deletedTemp = await db
    .delete(tempFiles)
    .where(lt(tempFiles.createdAt, sevenDaysAgo));

  return NextResponse.json({
    success: true,
    cleaned: {
      sessions: deletedSessions.rowCount,
      logs: deletedLogs.rowCount,
      tempFiles: deletedTemp.rowCount,
    },
  });
}

Step 5: Weekly Report Generation

// app/api/cron/weekly-report/route.ts
import { NextResponse } from "next/server";
import { verifyCronSecret } from "@/lib/cron";
import { db } from "@/db";
import { orders, users } from "@/db/schema";
import { gte, count, sum } from "drizzle-orm";
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function GET(req: Request) {
  const error = verifyCronSecret(req);
  if (error) return error;

  const oneWeekAgo = new Date(Date.now() - 7 * 86400000);

  const [orderStats] = await db
    .select({
      totalOrders: count(),
      totalRevenue: sum(orders.amount),
    })
    .from(orders)
    .where(gte(orders.createdAt, oneWeekAgo));

  const [userStats] = await db
    .select({ newUsers: count() })
    .from(users)
    .where(gte(users.createdAt, oneWeekAgo));

  await resend.emails.send({
    from: "reports@yourdomain.com",
    to: "admin@yourdomain.com",
    subject: `Weekly Report — ${new Date().toLocaleDateString()}`,
    html: `
      <h2>Weekly Report</h2>
      <table>
        <tr><td>New Orders</td><td>${orderStats.totalOrders}</td></tr>
        <tr><td>Revenue</td><td>$${orderStats.totalRevenue}</td></tr>
        <tr><td>New Users</td><td>${userStats.newUsers}</td></tr>
      </table>
    `,
  });

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

Step 6: Testing Cron Jobs Locally

// scripts/test-cron.ts
async function testCron(path: string) {
  const res = await fetch(`http://localhost:3000${path}`, {
    headers: {
      Authorization: `Bearer ${process.env.CRON_SECRET}`,
    },
  });
  console.log(`${path}:`, res.status, await res.json());
}

testCron("/api/cron/daily-digest");
testCron("/api/cron/cleanup");
testCron("/api/cron/weekly-report");

Need Automated Business Workflows?

We build web applications with scheduled tasks, automated reports, and workflow automation. Contact us to discuss your project.

cron jobsVercelscheduled tasksNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles