# 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.

Source: https://boringpush.com/blog/cron-vs-background-queues-nextjs

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/`:

- `hourly/wal-checkpoint.mjs`
- `daily/backup-sqlite.mjs`
- `weekly/log-rotate.mjs`
- `monthly/vacuum-databases.mjs`

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`):

```javascript
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

- [Next.js Background Jobs Without Redis: $0 BullMQ](/blog/nextjs-background-jobs-no-redis-bullmq-alternative) — the zero-dependency queue
- [Why Next.js Background Tasks Die in Production](/blog/why-nextjs-background-tasks-die-in-production) — solving serverless freezes
- [Free Vercel Cron Alternative: Jobs Every Minute](/blog/free-vercel-cron-alternative-every-minute) — system cron guide
