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.

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 pending and automatically retries with exponential backoff (min(300s, 5s × attempt)).
  • Exceeds maxAttempts: Job moves to dead state for inspection without blocking other work.

3. How it works under the hood

  1. 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.
  2. 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.
  3. 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-dead

Zero vendor accounts. Zero Redis memory overhead. Background work that just works.