deploy · tutorial · vps

Cron Beat Redis: $0 Background Workers

Scheduled jobs without Redis, BullMQ, or bills: system cron plus idempotent workers plus dead-letter rows. The $0 pattern inside.

A woodcut of a lamplighter walking down a street at night with a ladder over his shoulder and a lantern in his hand
A woodcut of a lamplighter walking down a street at night with a ladder over his shoulder and a lantern in his hand · Photo: Rijksmuseum, CC0, via Wikimedia Commons

A simple job needed three services

Daily digest emails. Simple, right?

The tutorial says: Redis for the queue, BullMQ for workers, a separate worker process, persistence config, monitoring for the queue.

Three services.

Two bills.

One afternoon of YAML.

To send emails once a day.

Every service you add is another thing that can fail without telling you.

A worker that exits quietly leaves jobs piling up where nobody looks.

Queue stack or scheduled jobs

A daily digest Redis and BullMQ A scheduled script
Services to run Redis, a worker, a monitor The scheduler already on your server
Extra cost A managed Redis plan, if you rent one None
Typical failure Eviction, a silent worker, a backlog The script fails and says so
Ceiling Millions of jobs Hourly and daily batches

Below real scale, queues are overkill. Three reasons.

You do not have the throughput. Millions of jobs an hour need a queue. Hundreds a day need a loop over database rows.

Every service is a pager. Persistence, worker liveness, queue depth. Three more things to watch at 2 a.m.

Silent workers are the real incident. A queue worker dies quietly while jobs pile up. A scheduled job that fails exits with an error and writes a log.

Audible failure beats a dashboard nobody opens.

What to do

Group your work by how often it runs.

Every minute.

Hourly.

Daily.

Weekly.

Monthly.

Write each job as a small script that does its work and exits.

Make every job safe to run twice. That is the whole secret of boring background work.

One unit of work per transaction. A crash costs you one row.

A job that can re-run fears nothing, including 3 a.m.

Make sure the same group never runs twice at once. A slow night must not stack runs on top of each other.

And make failure loud. A failed job should exit with an error and write its details to a log.

Where cron stops and queues take over

Scheduled cron fits calendar shapes: hourly, daily, weekly.

It does not fit event-driven user actions. If someone signs up and you need to send an email, you do not want a cron job polling your database every sixty seconds.

When a background task needs to run right away, retry on API errors, and keep payloads safe across server restarts, use a queue. BoringPush includes boringqueue for that work: one SQLite table, a worker thread pool, and zero Redis bills.

What BoringPush already runs

The kit ships the housekeeping most apps forget.

Daily, it takes a database snapshot and clears old analytics events. Hourly, it trims the database's write-ahead file so it cannot grow without limit.

Weekly, it rotates the logs. Monthly, it compacts the databases.

For transactional work, dispatchJob() pushes tasks to db/queue.db, where worker threads process them with automatic exponential backoff.

Know this first

Keep the boundary clear in your head.

Scheduled jobs (cron) run on a clock. They have no payload and no retry backoff.

Event jobs (boringqueue) run on demand. They take payloads, retry when APIs hiccup, and support delayed runs like +2h.

Neither requires Redis, BullMQ, or cloud queue subscriptions.

Why BoringPush is the way to do this

Both layers live right in your repo.

Cron for server maintenance. boringqueue for transactional background tasks.

Both run on SQLite on your $5 server. Zero external broker bills.

BoringPush runs hourly, daily, weekly and monthly jobs on the server you already have.

Backups, cleanup and log rotation are already in the box. The same group never runs twice at once.

Add a job by adding a script.

FAQ

Are background workers without Redis reliable enough for money paths?

Yes, with the two habits above: jobs that are safe to re-run and failures that are loud. Retries of failed charges or receipts fit a daily batch well. Reliability comes from re-runnability and visible failure, not from a queue brand.

How are overlapping runs prevented?

Each group of jobs takes a lock before it starts. If a previous run of the same group is still alive, the new one skips itself and says why. Watch how long your jobs take, because a runtime that keeps growing is an early warning that you are outgrowing the schedule.

Should jobs share the app process or run separately?

Separately. Each run starts fresh, so a crashing job cannot take the app down, and an app restart cannot kill a job halfway. Sharing a process couples the two uptimes.

How are job secrets handled?

Jobs read the same production settings as the app. You keep one settings file and one way of syncing it, so there is a single truth and many readers.

Can scheduled jobs retry with backoff?

Yes, if you store it in data. Keep a "try again after" time and an attempt count on the row, and let the schedule tick on a fixed beat. The schedule triggers; the rows decide who is due.

When do I actually need Redis and BullMQ?

When you need sub-minute scheduling with guarantees, priority lanes where paid jobs jump the queue, sustained throughput in the thousands per minute, or real-time fan-out. Measure first. If scheduled jobs demonstrably fail you, move over deliberately.

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