Automate Background / Cron-Tasks in Next.js
While Next.js is primarily designed for request-response interactions, real-world applications often require background processes or scheduled tasks. Common use cases include cleaning up expired database entries, sending periodic emails, or refreshing caches.
Since Next.js 13+, the Instrumentation hook allows to execute code once when the server starts, making it the perfect place to initialize cron jobs.
Runtime Environment Matters. This approach works best for long-running Node.js servers (VPS, Docker, …). If you are deploying to Vercel Serverless, this method may not persist because serverless functions spin down after execution. For Vercel, consider using Vercel Cron Jobs or find out about other solutions that meets your requirements.
Prerequisites
- An existing Next.js project (App Router)
- (A database setup (e.g., SQLite using
better-sqlite3) - optional, used in this guide) - Basic understanding of Cron syntax
The Use Case
In this example, I’m running a PGP Communication Platform .
Users can generate a PGP Keypair and register a mailbox to send and receive encrypted emails.
Because this is a demo and learning project for me, I don’t want to store encrypted messages or mailbox ids permanently.
I want to automatically delete messages and mailboxes that are older than 24 hours from my database, this is where the cron job comes into play.
The cron job registers my wipe-db-data method and runs it every 30 mins to check for messages and mailboxes that are older than 24h.
Implementation
I’m using node-cron to handle the scheduling and instrumentation.ts to start / register the process.
Install Dependencies
We need a task scheduler. node-cron is a robust choice for Node.js environments.
sh filename="Terminal" npm install node-cron npm install -D @types/node-cron
Create the Cron Logic (lib/cron.ts)
Create a file to define your scheduled tasks. We use a Singleton pattern (isCronStarted) to prevent the job from initializing multiple times during hot-reloading in development.
import cron from 'node-cron';
import { cleanupDb } from '@/lib/db';
let isCronStarted = false;
export function initCronJob() {
// Prevent duplicate execution during development hot-reloads
if (isCronStarted) return;
const getTimestamp = () => new Date().toLocaleString('de-DE');
console.log(`[${getTimestamp()}] Initializing Cronjobs...`);
// Interval: "*/30 * * * *" means every 30 minutes
cron.schedule('*/30 * * * *', () => {
const now = getTimestamp();
console.log(`[${now}] Starting scheduled database cleanup...`);
try {
// Execute the database cleanup function (you can replace this with any task you need)
cleanupDb();
// Log success
console.log(`[${getTimestamp()}] ✅ Database cleanup completed successfully.`);
} catch (error) {
// Catch and log errors so the main process doesn't crash
console.error(`[${getTimestamp()}] ❌ ERROR during database cleanup:`, error);
}
});
isCronStarted = true;
console.log(`[${getTimestamp()}] ✅ Cronjob is active and running in the background.`);
}Register the Hook (instrumentation.ts)
Create a file named instrumentation.ts in your project root (same level as package.json). Next.js automatically detects this file and runs the register function at boot.
Dynamic Imports are crucial here.
We must check process.env.NEXT_RUNTIME === 'nodejs' before importing the cron logic.
If we don’t, Next.js might try to bundle node-cron into the Edge Runtime (Middleware), which causes build failures because Edge environments don’t support file system access.
export async function register() {
// Only run this in the Node.js runtime, not Edge/Middleware
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Dynamically import to avoid bundling issues
const { initCronJob } = await import('@/lib/cron');
initCronJob();
}
}Verify Configuration (next.config.ts)
In recent Next.js versions, instrumentation is enabled by default. However, if you are on an older version or run into issues, ensure your config explicitly allows it or doesn’t conflict with experimental flags.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// standalone output is recommended for Docker/VPS deployments
output: "standalone",
experimental: {
//"instrumentationHook": true // No longer needed in latest Next.js 15+
serverActions: {
allowedOrigins: ['<your-domain.com>', 'localhost:3000'],
},
},
};
export default nextConfig;Testing the Automation
To verify your background task is running, start your development server. You should see the initialization logs immediately in your terminal.
[05.02.2026, 20:45:00] Initializing Cronjobs...
[05.02.2026, 20:45:00] ✅ Cronjob is active and running in the background.
...
[05.02.2026, 21:15:00] Starting scheduled database cleanup...
[05.02.2026, 21:15:00] ✅ Database cleanup completed successfully.
Debugging Tips
- Job runs twice: In
devmode, Next.js sometimes compiles the backend twice. Theif (isCronStarted) return;check in step 2 prevents duplicate jobs. - Timezone issues: Cron syntax runs based on the server’s system time. If your server is UTC but you expect CET, your jobs might run at unexpected hours.
Created: 06.02.2026
Last Updated: 05.02.2026