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.

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.
| Feature | SQLite Queue (boringqueue) | BullMQ + Managed Redis |
|---|---|---|
| Infrastructure | One file on local disk (queue.db) | Separate server or Docker container |
| Monthly cost | $0 extra on your VPS | $20 to $50 per month |
| Connection limits | Single local file descriptor | Max client caps on serverless spikes |
| Code sharing | Same repo, same imports, zero monorepo | Monorepo or build pipelines required |
| Survival across reboots | Written to disk before 200 OK | Depends 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-deadBoth 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
- Cron Beat Redis: $0 Background Workers — scheduled batch maintenance
- Free Inngest Alternative: Cron and Node Workers on a VPS — comparing cloud queues
- Why Next.js Background Tasks Die in Production — solving serverless freezes
+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)
Command from your phone.
Built so autonomous AI agents build what you want without breaking what's already paying you.
- Built-in analytics — cookie-free mode, $0 forever, no PostHog bill
- SQLite ↔ Supabase: one setting, same field names
- Full digital marketplace & feedback boards included
- 3 payment rails: Stripe, LemonSqueezy & Dodo Payments
- Agent fences: lint gate, read-only data access, auto-rollback
- Ship from your phone via SSH & WireGuard/Tailscale in under a minute
$199
Get BoringPushOne-time payment · Lifetime updates · Unlimited projects.
Keep reading


