NIXX/DEVv1.14.0
ArticlesFavorites
Sign In
Sign In
Articles

Welcome to our blog

A curated collection of insightful articles, practical guides, and expert tips designed to simplify your workflow

Cover image for: Deploying Next.js Apps with Cron Jobs and Background Workers
November 25, 20257 MIN READ min readBy ℵi✗✗

Deploying Next.js Apps with Cron Jobs and Background Workers

Take control of your Next.js app by running background jobs and scheduled tasks reliably — no hacks, no external services required.

next.jsBackground WorkersFull-Stack Development
ℵi✗✗

ℵi✗✗

Full-Stack Developer

Passionate about building tools and sharing knowledge with the developer community.

Was this helpful?

Popular Posts

  • NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    5 MIN READ min read

  • How to Enable HTTPS on Localhost in Under 2 Minutes

    How to Enable HTTPS on Localhost in Under 2 Minutes

    3 MIN READ min read

  • Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    4 MIN READ min read

  • Array Destructuring in PHP: A Practical Guide for Modern Developers

    Array Destructuring in PHP: A Practical Guide for Modern Developers

    5 MIN READ min read

Recommended Products

  • Apple MacBook Air M2

    Apple MacBook Air M2

    4.4
  • Samsung Galaxy S23

    Samsung Galaxy S23

    4.2
  • Apple iPad (7th Gen)

    Apple iPad (7th Gen)

    4.3
  • Fitbit Versa 4

    Fitbit Versa 4

    4.3

May contain affiliate links

Topics

webdev33productivity16cybersecurity12javascript11automation9guide8react7typescript7php6tutorial6freelancing5github actions5privacy5how to4Node.js4
+111 more topics →
🇺🇸USD ACCOUNTOpen a free US-based USD accountReceive & save in USD — powered by ClevaSponsoredInterserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored

Next.js is designed for serverless and server-side rendering, which creates a genuine difficulty for tasks that need to run periodically or outside the request-response cycle. A cron job registered inside an API route may never run on time if the serverless function is not already warm. A long-running task may be terminated before it completes. Multiple serverless instances can cause the same job to run more than once.

Reliable background jobs in Next.js require understanding this constraint and choosing a setup that accounts for it. This guide covers the options, from simple node-cron on a persistent server to BullMQ workers backed by Redis, with deployment considerations for each.

What this covers:

  • Why Next.js's execution model complicates background jobs

  • Three scheduling approaches and when to use each

  • Running node-cron locally and in production

  • BullMQ workers with Redis for durable job queues

  • Vercel Cron Jobs for serverless-compatible scheduling

  • Failure handling and retries

  • Monitoring and observability


Why Next.js Makes Background Jobs Harder

A traditional Node.js server or a long-running process keeps the same memory and event loop alive across requests. A cron job registered with node-cron on startup will fire at the scheduled time regardless of whether a request is incoming.

Serverless platforms including Vercel work differently. Each API route invocation spins up a function instance on demand. Between requests, there is no running process. node-cron registered inside an API route does not help because the code that registers the cron job only runs when the route receives a request, and the scheduled task only fires while that function instance is alive.

The specific problems this creates:

Cold starts. A scheduled task that relies on the server being warm will silently fail if no requests have kept the instance alive.

Duplicate executions. If multiple serverless instances are active simultaneously (which happens under load), a cron job registered in shared code could run in each instance concurrently.

Timeout limits. Serverless functions have maximum execution times (typically 10 to 60 seconds depending on platform and plan). A long-running background job will be killed if it exceeds this limit.

The solution depends on the hosting environment and the nature of the task.


Approach 1: node-cron on a Persistent Server

node-cron is the simplest solution and works correctly on platforms that run a persistent Node.js process: a VPS, Fly.io, Railway, or Render with a long-running web service.

Install the package:

npm install node-cron
npm install --save-dev @types/node-cron

Create a scheduler module that runs alongside the Next.js server:

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

export function startScheduler(): void {
    cron.schedule('0 * * * *', async () => {
        console.log('Hourly task started:', new Date().toISOString());
        try {
            await syncExternalData();
        } catch (err) {
            console.error('Hourly task failed:', err);
        }
    });

    cron.schedule('0 2 * * *', async () => {
        console.log('Daily cleanup started:', new Date().toISOString());
        try {
            await cleanupOldLogs();
        } catch (err) {
            console.error('Daily cleanup failed:', err);
        }
    });

    console.log('Scheduler initialised');
}

Call startScheduler() from a custom server entry point:

// server.ts
import { createServer } from 'http';
import { parse } from 'url';
import next from 'next';
import { startScheduler } from './src/lib/scheduler';

const app = next({ dev: process.env.NODE_ENV !== 'production' });
const handle = app.getRequestHandler();

app.prepare().then(() => {
    startScheduler();

    createServer((req, res) => {
        const parsedUrl = parse(req.url!, true);
        handle(req, res, parsedUrl);
    }).listen(3000, () => {
        console.log('Server ready on port 3000');
    });
});

This approach is straightforward but requires a platform that keeps the process running. It is not suitable for Vercel.


Approach 2: BullMQ Workers with Redis

For tasks that are heavy, long-running, or need reliable exactly-once execution, a job queue with a dedicated worker process is the correct architecture. BullMQ uses Redis as a durable store for the job queue, which means jobs survive server restarts and are not lost if a worker crashes.

Install the dependencies:

npm install bullmq ioredis

Define the queue and worker in separate files:

// src/lib/queue.ts
import { Queue } from 'bullmq';
import IORedis from 'ioredis';

const connection = new IORedis(process.env.REDIS_URL!, {
    maxRetriesPerRequest: null,
});

export const emailQueue = new Queue('email', { connection });
export const reportQueue = new Queue('report', { connection });
// workers/email.worker.ts
import { Worker, Job } from 'bullmq';
import IORedis from 'ioredis';

const connection = new IORedis(process.env.REDIS_URL!, {
    maxRetriesPerRequest: null,
});

interface EmailJobData {
    to: string;
    subject: string;
    body: string;
}

const worker = new Worker<EmailJobData>(
    'email',
    async (job: Job<EmailJobData>) => {
        console.log(`Processing email job ${job.id} to ${job.data.to}`);
        await sendEmail(job.data);
    },
    {
        connection,
        concurrency: 5,
    }
);

worker.on('completed', (job) => {
    console.log(`Job ${job.id} completed`);
});

worker.on('failed', (job, err) => {
    console.error(`Job ${job?.id} failed:`, err.message);
});

Add jobs to the queue from API routes:

// app/api/send-email/route.ts
import { NextResponse } from 'next/server';
import { emailQueue } from '@/lib/queue';

export async function POST(req: Request) {
    const { to, subject, body } = await req.json();

    await emailQueue.add(
        'send',
        { to, subject, body },
        {
            attempts: 3,
            backoff: { type: 'exponential', delay: 5000 },
        }
    );

    return NextResponse.json({ queued: true }, { status: 202 });
}

The attempts: 3 and backoff configuration tell BullMQ to retry failed jobs up to three times with exponential backoff between attempts. This handles transient failures (network timeouts, downstream service unavailability) automatically.

Run the worker as a separate process:

ts-node workers/email.worker.ts

In production, manage the worker process with PM2:

pm2 start workers/email.worker.ts --interpreter ts-node --name email-worker

Approach 3: Vercel Cron Jobs

For applications hosted on Vercel that need scheduled execution, Vercel Cron Jobs are the native solution. They call an API route on a schedule defined in vercel.json. The API route handles one execution and exits.

Configure the schedule in vercel.json:

{
    "crons": [
        {
            "path": "/api/cron/sync",
            "schedule": "0 * * * *"
        },
        {
            "path": "/api/cron/cleanup",
            "schedule": "0 2 * * *"
        }
    ]
}

Create the API route:

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

export const maxDuration = 60;

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

    await syncExternalData();
    return NextResponse.json({ completed: true });
}

The CRON_SECRET check verifies that the request is from Vercel rather than a random external caller. Set this environment variable in both the Vercel project settings and .env.local.

maxDuration extends the function timeout for this route. Vercel adds this to the function configuration on deployment.


Failure Handling

For node-cron jobs, wrap the task in a try/catch and log both success and failure. Without this, a thrown error in a scheduled task fails silently:

cron.schedule('0 * * * *', async () => {
    try {
        await task();
        console.log('Task completed:', new Date().toISOString());
    } catch (err) {
        console.error('Task failed:', err);
        // optionally: send an alert, increment a metric, write to a database
    }
});

BullMQ handles retries automatically when attempts is configured. For tasks that must not be retried after a certain type of failure, the worker can inspect the error and decide:

const worker = new Worker('email', async (job) => {
    try {
        await sendEmail(job.data);
    } catch (err) {
        if (err instanceof PermanentFailureError) {
            // mark as failed without retrying
            await job.moveToFailed(err, job.token ?? '');
        }
        throw err; // rethrow to trigger retry
    }
}, { connection });

Monitoring

The minimum useful monitoring for any background job system is:

  • Log the start time, completion time, and outcome of every job execution

  • Log errors with enough context to diagnose the failure

  • Alert when a job fails after exhausting retries

For BullMQ, Bull Board provides a web UI that shows queue status, job counts by state, and individual job details:

npm install @bull-board/express @bull-board/api
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
import { emailQueue } from './src/lib/queue';

const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/admin/queues');

createBullBoard({
    queues: [new BullMQAdapter(emailQueue)],
    serverAdapter,
});

app.use('/admin/queues', serverAdapter.getRouter());

Protect this route with authentication before deploying.


Deployment Summary

Platform

Recommended approach

VPS, Fly.io, Render (persistent)

node-cron via custom server, or BullMQ worker process

Railway

BullMQ with Railway's Redis add-on, separate worker service

Vercel

Vercel Cron Jobs for scheduled tasks; BullMQ for async queues requires a separate worker host

Docker Compose

Separate worker container alongside the Next.js container


Key Takeaways

  • Serverless platforms do not keep processes alive between requests. A cron job registered inside an API route will not run reliably on Vercel or similar platforms.

  • node-cron works correctly on persistent server deployments. Use a custom server entry point to initialize the scheduler alongside the Next.js server.

  • BullMQ with Redis is the correct choice for durable, reliable job queues. It handles retries, persistence, and concurrent workers.

  • Vercel Cron Jobs are the native solution for scheduled execution on Vercel. Protect the endpoint with a secret to prevent unauthorized invocations.

  • Always wrap scheduled tasks in try/catch. Silent failures in background jobs are difficult to detect and debug.

  • Bull Board provides a useful monitoring UI for BullMQ queues without requiring custom logging infrastructure.


Conclusion

Background jobs in Next.js require choosing the right approach for the hosting environment. node-cron for simple scheduled tasks on persistent servers, BullMQ for durable and scalable job queues, and Vercel Cron Jobs for serverless-native scheduled execution. Each trades off complexity for reliability in different ways.

The common thread is that reliable background processing requires explicit failure handling and observability. A job that silently fails is worse than a job that does not exist, because the system appears to be working when it is not.


Working on a specific background job scenario in Next.js and running into a problem? Describe it in the comments.

Topics
next.jsBackground WorkersFull-Stack Development
Interserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored🇺🇸USD ACCOUNTOpen a free US-based USD accountReceive & save in USD — powered by ClevaSponsored

Discussion

Join the discussion

Sign in to share your thoughts and engage with the community.

Sign In
Loading comments…

Continue Reading

More Articles

View all
Cover image for: The 3-Device Rule: How to Simplify Your Digital Life and Stop Overbuying Tech
Aug 5, 20255 MIN READ min read

The 3-Device Rule: How to Simplify Your Digital Life and Stop Overbuying Tech

Tired of juggling too many devices? Learn the 3-device rule that helps you streamline your digital life, reduce clutter, and focus on what really matters.

Cover image for: AI for DevOps: Tools That Are Already Changing the Game
Jun 17, 20256 MIN READ min read

AI for DevOps: Tools That Are Already Changing the Game

How artificial intelligence is transforming CI/CD pipelines, monitoring, and incident response—today.

Cover image for: Build a Fun Alphabet Reader with TypeScript, Vite & Speech Synthesis API
Jun 27, 20254 MIN READ min read

Build a Fun Alphabet Reader with TypeScript, Vite & Speech Synthesis API

An interactive, educational project for beginners to learn modern frontend development.

Cover image for: React Authentication with JWT: A Step-by-Step Guide
Oct 17, 20257 MIN READ min read

React Authentication with JWT: A Step-by-Step Guide

Learn how to implement secure JWT authentication in React. From login to route protection and API calls, this guide covers everything you need to know.

|Made with · © 2026|TermsPrivacy
AboutBlogContact

Free, open-source tools for developers and creators · Community driven