
Photo by Anton Makarenko on pexels.com
In this article, we’ll explore how to implement Cron Jobs in NextJS. NextJS offers multiple methods for Cron Job implementation. We’ll discuss two of the best methods based on project length and user base.
Setting up scheduled tasks in a Next.js app can be challenging, especially without Vercel. Most guides suggest Vercel’s cron jobs or node-cron, but node-cron feels basic for serious projects. BullMQ, after trial and error, proved reliable, handles retries, and excels at cron jobs.
If your site has a small user base or you just started using Nexjs, use Method 1. If your site has a large user base and needs a more reliable method, use Method 2.
Both methods work the same but have different implementations.
Method 1. (Using npm Package)
For deploying NextJS on Vercel or any serverless platform, traditional node-cron won’t work since there’s no always-running server process. Instead, we’ll use the “cron” package, which has many downloads and is easy to implement for NextJS APIs.
Node-cron may cause issues in Next APIs since API routes are stateless, leading to multiple cron registrations on each request.
The “cron” package is safe for NextApp as it executes jobs external to the JavaScript process using child_process. Its implementation is simple and clean.
Installation
npm i cron
Let’s take an example: you want to hit any DELETE route in your project every day.
src/app/api/your_file_name/route.ts
import { CronJob } from 'cron';
export async function DELETE() {
.... your logic
}
const job = new CronJob(
'0 0 13 * * *', // cronTime
async function () {
await DELETE();
…..
// here you can use any alert like sending emails after cron gets run etc…
}, // onTick
null, // onComplete
true, // start
'Asia/Kolkata' // timeZone
);
job.start(); // Optional as we lardy start our jobs using 4th parameter =true
Now this cron will hit This DELETE request every day at 1PM.
Method 2 (using Redis and BullMQ)
Step 1: Set Up Redis
BullMQ requires Redis for job storage and management. Here’s how to set it up:
Mac: bash
brew install redis
Linux: bash
sudo apt install redis-server
Start Redis (don’t forget this, or nothing works):
bash
redis-server
Install the packages:
npm install bullmq ioredis
Step 2: File Setup
Keep it simple with this structure:
📁 server/
scheduler.js
📁 jobs/
emailReminders.js
Step 3: The Scheduler Code
Here’s a stripped-down scheduler that runs jobs on a schedule:
server/scheduler.js
const { Queue, Worker } = require('bullmq');
const Redis = require('ioredis');
const { sendEmailReminders } = require('./jobs/emailReminders');
// Connect to Redis
const redis = new Redis('redis://localhost:6379');
const jobQueue = new Queue('tasks', { connection: redis });
// Define jobs
const jobs = [
{
name: 'email-reminders',
schedule: '0 9 * * *', // 9 AM daily
task: sendEmailReminders,
},
];
// Clear old jobs
async function clearJobs() {
const oldJobs = await jobQueue.getRepeatableJobs();
for (let job of oldJobs) {
await jobQueue.removeRepeatableByKey(job.key);
console.logCleared old job: ${job.name});
}
}
// Start scheduler
async function start() {
console.log('Starting scheduler...');
await clearJobs();
// Schedule jobs
for (let job of jobs) {
await jobQueue.add(
job.name,
{},
{ repeat: { pattern: job.schedule } }
);
console.logScheduled: ${job.name});
}
// Run jobs
const worker = new Worker(
'tasks',
async (job) => {
const theJob = jobs.find((j) => j.name === job.name);
if (!theJob) {
console.logUnknown job: ${job.name});
return;
}
console.logRunning: ${job.name});
await theJob.task();
console.logDone: ${job.name});
},
{ connection: redis }
);
worker.on('failed', (job, err) => {
console.logJob ${job.name} failed: ${err.message});
});
}
start().catch((err) => console.log('Error:', err));
What’s happening here?
Redis Connection: Connects to Redis at localhost:6379.
Queue: Creates a queue called tasks
to hold jobs.
Jobs Array: Lists jobs with their names, cron schedules (e.g., 0 9 * * *
for 9 AM daily), and the function to run.
Clear Jobs: Removes old jobs to avoid duplicates from past runs.
Scheduler: Adds each job to the queue with its schedule.
Worker: Processes jobs when their time comes, running the matching function.
Error Handling: Logs failures so you know what went wrong.
Step 4: A Sample Job
Here’s a basic job to send email reminders:
server/jobs/emailReminders.js
async function sendEmailReminders() {
console.log('Sending reminders...');
// Fake database call
const users = [
{ email: 'john@example.com', name: 'John' },
{ email: 'jane@example.com', name: 'Jane' },
];
for (let user of users) {
console.logEmail to ${user.name} (${user.email}));
// Add real email logic (e.g., nodemailer) here
}
console.logSent ${users.length} emails);
}
module.exports = { sendEmailReminders };
What’s this doing?
Pretends to fetch users from a database (replace with real database code).
Loops through users and logs a message for each (swap in email-sending code).
Logs how many emails were “sent.”
Step 5: Run It
Add this to package.json
pm2 start "npm run scheduler" --name "scheduler"
Run the scheduler:
npm run scheduler
For production, use PM2 to keep it running:
pm2 start "npm run scheduler" --name "scheduler"
Why This Works
Better than node-cron: BullMQ stores jobs in Redis, handles retries, and tracks status—node-cron just runs tasks and forgets.
-No Vercel needed: Full control, no platform lock-in.
Simpler than AWS/GitHub: Everything stays in the codebase, no external services.
Tips
Redis must stay up: If it crashes, jobs stop. Keep an eye on it.
Cron syntax is tricky: 0 9 * * *
means 9 AM daily. Test patterns online to avoid mistakes.
Error handling: Always log errors to debug issues.
Clean old jobs: The clearJobs
function prevents old jobs from piling up.
Debugging
For a quick look at what’s running, use BullMQ’s dashboard:
npm install -g @bull-board/cli
in terminal
bull-board --redis redis://localhost:6379
Check http://localhost:3000
to see job statuses.
Wrap Up
This setup is simple, reliable, and straightforward. Start with one job, add more as needed, and it works. No late-night debugging or weird failures. Get Redis running, copy the code, tweak the jobs, and it’s ready to go.