Boring Docs
Event-Driven Background Workers — Zero Redis, Zero Cloud Queues
Dispatch background jobs, retry failed tasks with exponential backoff, and schedule delayed runs right inside your app.
In this guide
Event-Driven Background Workers
When someone pays, signs up, or requests a 45-second AI summary, your web server should answer in milliseconds. Heavy work belongs in the background.
The standard industry recommendation is to add a managed Redis cluster, configure BullMQ, or pay $35/mo+ for Inngest or Upstash. That means more bills, more vendor dashboards, and more moving parts to break.
BoringPush includes a built-in event-driven background worker engine. It runs on local SQLite, processes tasks concurrently in worker threads, retries flaked APIs with exponential backoff, and requires zero external services.
When to use this vs. Scheduled Folder Jobs
| Task Type | What to Use | Example |
|---|---|---|
| Calendar rhythms (hourly, daily, weekly) | Scheduled Folders (scripts/workers/) |
Nightly database snapshots, hourly WAL checkpoints, weekly log rotation. |
| Event-driven actions (immediate or delayed) | Timely Background Workers (dispatchJob()) |
Sending welcome emails on signup, unlocking access when Stripe confirms, running long AI workflows. |
1. Dispatch a job in one line
Import dispatchJob anywhere in your API routes, server actions, or webhooks:
import { dispatchJob } from "@/libs/boringqueue";
// Run immediately on the next available worker thread:
await dispatchJob("emails/welcome", { userId: user.id });Delaying a job (drip sequences)
Want to send a check-in email 2 days after signup? Pass a runAt offset:
await dispatchJob("emails/followup", { userId: user.id }, {
runAt: "+2d", // "+90s", "+15m", "+2h", "+2d", or an ISO Date string
});Safe idempotency & automatic retries
If an external API (like Stripe or Resend) is temporarily down, the worker automatically retries with exponential backoff:
await dispatchJob("orders/fulfill", { orderId: session.id }, {
referenceKey: `order-${session.id}`, // Deduplicates if the webhook delivers twice
maxAttempts: 5, // Retries up to 5 times on failure
});2. Write your job handler
A job is simply a file inside scripts/jobs/<folder>/<name>.mjs.
There is no complex registration step or broker configuration. Dropping a file into scripts/jobs/ creates the job:
// scripts/jobs/emails/welcome.mjs
import { findUserById } from "../../../models/User.js";
import { sendEmail } from "../../../libs/resend.js";
export default async function (payload, { job, signal }) {
const user = await findUserById(payload.userId);
if (!user) return; // Nothing to do
// signal automatically aborts if the job reaches its timeout
await sendEmail({
to: user.email,
subject: "Welcome to BoringPush",
html: "<p>Your account is ready.</p>",
});
// Returning exits cleanly and marks the job as done.
// Throwing an error triggers an automatic retry with backoff.
}The retry contract:
- Return cleanly: Job is marked
done(recorded for history). - Throw an error: Job returns to
pendingand automatically retries with exponential backoff (min(300s, 5s × attempt)). - Exceeds
maxAttempts: Job moves todeadstate for inspection without blocking other work.
3. How it works under the hood
- Local SQLite Persistence (
db/queue.db): Every job is written in a fast ACID transaction. If your server restarts or reboots, pending jobs are never lost. - Dedicated Worker Thread Pool: Tasks run in Node worker threads (
worker_threads), preventing heavy computation or long HTTP requests from freezing your main web server. - Automatic Recovery: If a worker thread crashes or hangs unexpectedly, the pool replaces the thread, rescues the job, and marks it for retry.
4. Managing jobs from the terminal
Inspect your background workers and jobs anytime using the BoringPush CLI:
# View active, pending, and completed jobs
bp jobs list
# View queue stats (pending, processing, done, dead)
bp jobs counts
# Inspect a specific job's payload and error trace
bp jobs show <jobId>
# Retry any dead jobs that exhausted their attempts
bp jobs retry-deadZero vendor accounts. Zero Redis memory overhead. Background work that just works.