nextjs · workers · backend · sqlite

Next.js Background Jobs Without Redis: $0 BullMQ

You do not need Redis or BullMQ for background jobs in Next.js. How to run SQLite worker threads on a $5 VPS with zero external bills.

An architectural diagram contrasting a heavy Redis BullMQ cluster with a clean single-table SQLite queue in Next.js
An architectural diagram contrasting a heavy Redis BullMQ cluster with a clean single-table SQLite queue in Next.js · Photo: BoringPush

Every Next.js tutorial repeats the same prescription for background tasks.

You need Redis. You need BullMQ. You need a separate worker container, an Upstash subscription, and an afternoon writing Docker Compose files.

All of that to send a welcome email and sync a Stripe receipt.

The multi-service tax for simple async work

Modern frameworks encourage developers to rent another service for every tiny requirement.

A user signs up on your landing page. You want to send an email, create an onboarding record, and ping your analytics without slowing down the HTTP response.

If you run on serverless platforms, unawaited promises terminate the instant your API route returns. If you follow standard tutorials, you spin up Redis, install BullMQ, configure persistent connections, and set up a queue monitor.

That means two monthly bills, three points of failure, and hundreds of lines of boilerplate.

When your app is young, you do not have millions of jobs per second. You have tens or hundreds of jobs per day.

Paying twenty to fifty dollars every month for a managed queue broker is pure waste.

What a background queue actually needs

Strip away the marketing copy of hosted queue platforms. At its core, an async job runner needs four mechanical properties:

First, an atomic place to insert tasks with a JSON payload and an execution timestamp.

Second, a worker process that claims pending jobs safely without race conditions.

Third, automatic retries with exponential backoff so temporary third-party API outages do not drop customer data.

Fourth, a dead-letter state so you can inspect failed jobs and replay them when the upstream API recovers.

You can build all four on top of a single database table.

Why SQLite beats Redis for solo background queues

Redis is an in-memory cache with optional disk persistence. SQLite is an embedded, ACID-compliant relational database that lives in a single file on your disk.

When you use SQLite in WAL mode (Write-Ahead Logging), readers never block writers, and writers never block readers. A Next.js API route can insert a job row in less than one millisecond.

A worker thread running on the same machine polls that table, claims rows inside a transaction, and executes the task.

FeatureSQLite Queue (boringqueue)BullMQ + Managed Redis
InfrastructureOne file on local disk (queue.db)Separate server or Docker container
Monthly cost$0 extra on your VPS$20 to $50 per month
Connection limitsSingle local file descriptorMax client caps on serverless spikes
Code sharingSame repo, same imports, zero monorepoMonorepo or build pipelines required
Survival across rebootsWritten to disk before 200 OKDepends on Redis snapshot config

When your server reboots, the jobs are still there on disk. When your traffic spikes, you do not hit socket connection limits.

How BoringQueue works in practice

In BoringPush, the queue engine lives in libs/boringqueue/. It operates on a dedicated database file, db/queue.db, so queue operations never compete with your primary application data.

To queue work from any Next.js route or server action, you call one function:

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

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

  // Dispatch and return immediately
  dispatchJob("emails/welcome", { email, userId }, {
    maxAttempts: 5,
    runAt: "+30s",
  });

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

The job itself is a plain JavaScript file located under scripts/jobs/<group>/<name>.mjs:

export default async function (payload, { job, signal }) {
  const { email, userId } = payload;
  
  // Real work happens here
  await sendWelcomeEmail({ to: email, id: userId }, { signal });
  
  // Return = completed
  // Throw = automatic retry with exponential backoff
}

The worker loop runs under PM2 alongside your Next.js application.

If the email provider is temporarily down, the worker catches the error, marks the job for retry, and backs off: min(300s, 5s * attempt).

If the job fails five times in a row, it moves to the dead state. Nothing is deleted.

Inspecting and rescuing jobs from the terminal

Web dashboards like Bull Board or Inngest Cloud look pretty, but they are useless when you are debugging an issue from your phone over SSH.

Because BoringPush is agent-first, the queue includes a CLI command:

# List all pending, running, and failed jobs
npm run bp -- jobs list

# View failure counts across categories
npm run bp -- jobs counts

# Retry all failed dead-letter jobs after fixing an API key
npm run bp -- jobs retry-dead

Both human developers and autonomous agents like Claude Code can run these commands. The CLI outputs clean JSON with exit codes, making incident recovery fast and deterministic.

Where SQLite queues stop

Honesty matters in systems architecture. A single-table SQLite queue is not the solution for every company on earth.

If you process ten thousand jobs per second across twenty distributed worker machines, you need a distributed broker like Apache Kafka, RabbitMQ, or AWS SQS.

If you need multi-tenant step functions with visual dependency graphs, an orchestration platform earns its price tag.

For the first ninety-nine percent of SaaS applications, your bottleneck is not queue throughput. Your bottleneck is complexity and unneeded infrastructure bills.

Start with the simplest tool that guarantees durability and retries.

Why BoringPush includes this out of the box

Most boilerplates leave background jobs as an exercise for the reader. They tell you to bring your own Redis or sign up for a cloud queue.

BoringPush ships with boringqueue pre-configured.

The worker script, the SQLite migration, the thread pool, and the CLI tools are already wired into the repository. When you deploy your application to a $5 VPS, background processing works on day one.

Zero Redis containers. Zero monthly subscriptions.

FAQ

Does running workers on the same VPS slow down Next.js?

No. The worker runs in a separate thread pool managed by PM2. On a standard 2-core VPS, background tasks like sending emails or processing webhooks consume negligible CPU and memory.

What happens if the server reboots while a job is running?

When the worker boots back up, it runs an automatic rescue check. Jobs that were marked as running but have stale heartbeats are reclaimed and retried safely.

Can I schedule a job to run in the future?

Yes. The runAt parameter accepts relative time strings like +5m, +2h, or +1d, as well as epoch millisecond timestamps and ISO dates.

How does this compare to system cron?

System cron is designed for time-based recurring maintenance, like nightly database backups. It cannot receive runtime payloads or retry failed tasks. boringqueue is an event-driven queue designed for dynamic application tasks with payloads and automatic backoff.

Can I use this with MongoDB or Supabase?

Yes. Even if your primary application database is Supabase or MongoDB, boringqueue uses its own tiny SQLite file (db/queue.db) on local disk for maximum speed and zero network latency.

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