nextjs · backend · deploy · workers

Cron vs. Background Queues in Next.js: The Hard Line

Cron is not a queue. When to use system cron for maintenance vs when you need a real job queue with retry backoff and payloads in Next.js.

A clock representing cron schedules beside an active task queue in Next.js
A clock representing cron schedules beside an active task queue in Next.js · Photo: BoringPush

Every growing Next.js project reaches a moment where code needs to run outside the user's browser request.

Maybe you need to back up your database at night. Maybe you need to deliver a welcome email after payment. Maybe you need to rotate access logs before the hard drive fills up.

Too many developers reach for the wrong tool. They treat cron like a queue, or they install a heavy queue to do the job of a simple cron.

Both mistakes lead to production headaches.

The fundamental difference in one sentence

System cron runs on a calendar clock; background queues run on application events.

Cron does not know or care who your users are. It wakes up at three o'clock in the morning, runs a command, and goes back to sleep.

A queue does not care what time it is. It wakes up the instant an event happens, carries a specific payload of data, and guarantees execution even if an upstream API is down.

Confusing these two concepts creates brittle architectures.

The disaster of using Cron as a Queue

Here is a common pattern that starts innocently and ends in disaster.

A founder wants to send welcome emails asynchronously. Rather than configuring a background queue, they add a column to their user table: emailSent: false.

Then, they write a cron job that runs every sixty seconds to find users where emailSent is false, send the email, and mark the row true.

This works when you have ten users. At real volume, it falls apart in four distinct ways:

First, there is latency. A customer signs up and waits up to fifty-nine seconds before their verification link arrives. That delay kills onboarding conversion.

Second, there is the concurrency overlap. If your email provider takes longer than sixty seconds to process a batch, the next cron tick fires while the first is still running. Both processes grab the same rows, and your customers receive duplicate emails.

Third, there is no backoff. If your email provider experiences an outage, your cron script crashes every single minute, spamming your error alerts until someone wakes up.

Fourth, you lose context. Cron scripts poll the database without the original request metadata, making fine-grained tracing impossible.

Cron was never meant to process dynamic transactional streams.

The disaster of using Queues for Cron

The reverse mistake is equally common.

A developer installs BullMQ, provisions a Redis server, and writes a background worker just to trigger a nightly database backup.

Now, you have a long-running Redis connection holding open sockets, consuming memory, and waiting twenty-four hours just to run one shell command.

If your Redis container runs out of memory or crashes during the day, your nightly backup silently fails to fire.

For scheduled housekeeping, cron on your operating system is already built, already tested, and costs zero dollars.

Where the boundary sits

To keep your Next.js architecture clean, follow this operational decision matrix:

Task Type Correct Tool Why
Nightly SQLite backup (VACUUM INTO) System Cron Fixed time, zero payload, system maintenance
Hourly WAL checkpoint System Cron Fixed time, prevents disk bloat, no user context
Weekly log rotation System Cron Operating system level housekeeping
Welcome email on sign-up Background Queue Triggered by user action, carries { email, userId }, needs 0s latency
Stripe checkout webhook processing Background Queue Must return 200 OK fast, needs retries if DB is locked
Scheduled social media post (+2h) Background Queue Delayed execution, unique deduplication key, retry backoff
AI video transcode or PDF export Background Queue Heavy compute, takes minutes, needs worker concurrency pool

When the work is tied to the calendar, use cron. When the work is tied to a user or external webhook, use a queue.

How BoringPush implements both without third-party services

BoringPush ships with both layers fully implemented, without relying on Redis, BullMQ, or cloud queue vendors.

Layer 1: OS Cron for Housekeeping

Periodic maintenance scripts live in scripts/workers/:

When you deploy your application, setup registers a single crontab under the non-root application user.

Each group uses an atomic lock file. If a daily task runs long, the next run skips cleanly rather than stacking processes on top of each other.

Layer 2: BoringQueue for Transactional Work

Event-driven background jobs live in libs/boringqueue/.

When an action occurs in a Next.js route, you dispatch the job directly to an embedded SQLite database (db/queue.db):

import { dispatchJob } from "@/libs/boringqueue";

export async function POST(req) {
  const { userId, plan } = await req.json();

  // Instant dispatch to SQLite
  dispatchJob("billing/provision", { userId, plan }, {
    maxAttempts: 5,
    referenceKey: `prov-${userId}`,
  });

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

A dedicated worker pool managed by PM2 picks up the job immediately.

If the provisioning script encounters an error, it retries using exponential backoff: min(300s, 5s * attempt).

If you dispatch the same job twice with the same referenceKey, the queue deduplicates the task automatically.

Summary: Stop paying for over-engineered stacks

You do not need a twenty-dollar cloud scheduler for cron. You do not need a fifty-dollar Redis broker for queues.

Your operating system already knows how to tell time. A local SQLite database already knows how to store persistent rows.

Use cron for the clock. Use a queue for the work. Keep both on your server and keep your invoices at zero.

FAQ

Can I run cron inside my Next.js codebase using node-cron?

You can, but you should not. Running node-cron inside a Next.js process means your schedule is tied to your web server lifecycle. If you run multiple web worker instances, your cron tasks will execute multiple times. Use system-level cron on the host instead.

How does BoringQueue prevent duplicate job execution?

boringqueue uses SQLite transactions with immediate locking to claim pending rows. When a worker thread claims a job, it marks the status as running within an atomic transaction, preventing other threads from seeing or claiming the same row.

What happens to cron jobs on serverless platforms like Vercel?

Serverless platforms do not have a persistent OS cron. They simulate cron by sending automated HTTP GET requests to your API routes on a schedule. This consumes serverless function execution quotas and is subject to route timeout limits.

How do I test background jobs locally?

In BoringPush, you run npm run dev in one terminal and npm run bp -- jobs work in another. The worker processes dispatched jobs immediately against your local development database.

What is the maximum payload size for a background job?

Store references, not massive data blobs. Store the userId or orderId in the job payload and let the worker fetch fresh data from your database, rather than passing megabytes of JSON through the queue table.

Keep reading

+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)

The Agent-First Shipping Kit

Command from your phone.

Built so autonomous AI agents build what you want without breaking what's already paying you.

$199

Get BoringPush

One-time payment · Lifetime updates · Unlimited projects.

Keep reading