Back to blog

BullMQ v6 Dropped Repeatable Jobs: Your Recurring Jobs Will Stop

BullMQ v6 removed the repeat option and the Repeatable Jobs API in favour of Job Schedulers. Upgrade without migrating and your recurring jobs stop firing silently.

CronGuard TeamCron Job Monitoring Experts
6 min read
A laptop screen at an angle in a dark room lit blue and purple, showing a code editor with a file tree on the left and JavaScript import statements open in the main pane

The upgrade that quietly stops your recurring jobs

BullMQ v6 landed on 30 July 2026, and it is not a drop-in upgrade if you run recurring jobs. The repeat option that scheduled a job every night, the Repeat class, and the whole Repeatable Jobs API are gone. Not deprecated with a warning. Removed. Leave your old enqueue code in place and the upgrade compiles, the queue starts, workers report healthy. The nightly job never fires again.

This is the failure BullMQ shares with every scheduler: nothing errors. You notice when a downstream report is three weeks stale.

What changed in BullMQ v6

Repeatable jobs are gone, not deprecated

v6 removed the repeat option from Queue.add and Queue.addBulk, deleted the Repeat class, and dropped getRepeatableJobs, removeRepeatable, and removeRepeatableByKey. The same release made the queue backend pluggable across Redis and Postgres, so the upgrade often rides along with a backend swap and the scheduling regression hides in the noise.

Job Schedulers are the replacement

Recurring work now goes through a Job Scheduler, created with upsertJobScheduler. It owns a repeat rule and a job template, and produces the jobs on cadence. The verb matters: upsert, not add, so redeploying the same scheduler id updates the rule in place instead of stacking a second schedule on it.

Why the migration fails silently

The repeat option becomes a no-op

TypeScript catches a leftover repeat option, because the type no longer allows it. Plain JavaScript catches nothing: the property is ignored, add treats the call as a one-off, and the job runs once and never again. A queue that fired every hour fires one last time on deploy and goes quiet.

The pattern field counts seconds

BullMQ parses cron patterns with cron-parser, bumped to 5.7.0 in v6.0.6 on 3 August 2026. cron-parser accepts an optional sixth field for seconds at the front, so 0 15 3 * * * means 03:15:00, not a quarter past three every hour. A five-field crontab line still works, but paste one that already carries a seconds column and every field shifts a place. Check the parsed next-run time before you trust a copied expression.

One scheduler, one delayed job

A Job Scheduler keeps exactly one job in the delayed state, and produces the next occurrence only when the current one starts processing. On a healthy queue that is invisible. On a congested queue, where the previous job is stuck behind a backlog or wedged in active after a crash, the next run is never produced. The schedule stops. Nothing errors.

Migrating a repeatable job to a scheduler

The mapping is clean. What was this:

// BullMQ v5 and earlier: removed in v6
await queue.add(
  "nightly-report",
  { tenantId: 42 },
  { repeat: { pattern: "0 3 * * *", tz: "Europe/Amsterdam" } },
);

becomes this:

// BullMQ v6
await queue.upsertJobScheduler(
  "nightly-report",                          // stable scheduler id
  { pattern: "0 3 * * *", tz: "Europe/Amsterdam" },
  { name: "nightly-report", data: { tenantId: 42 } },
);

The first argument is the scheduler id, the one thing you must keep stable across deploys. Reuse it and upsert updates the existing schedule. Derive it from a timestamp or a pod name and every rollout registers a new scheduler firing on its own. Tie the id to the job's identity, not the process that created it.

Removing the old repeatable entries

Code is not the whole migration. Repeatable definitions from v5 still sit in Redis, and the removal APIs that cleaned them up are gone. A v6 process pointed at a v5-populated Redis can be left with orphaned repeat keys that schedule nothing but clutter the keyspace. Recreate the schedules against a clean namespace, or migrate the keys deliberately.

Timezone handling moved to tz

The scheduler takes a tz option and honours DST for you. An omitted tz runs in the server's zone, so a container that moves region carries its schedule with it. Set tz explicitly on every scheduler that cares about wall-clock time.

Monitor the outcome, not the scheduler

Every failure here is invisible from inside BullMQ. A no-op repeat looks like a successful add. A stalled scheduler looks like a quiet queue. The built-in dashboard cannot flag a job that was never produced.

The check that survives all of it is external and success-based. Have the worker ping a monitor only after the real work finishes, and alert when the ping does not arrive on schedule:

const worker = new Worker("nightly-report", async (job) => {
  await buildAndSendReport(job.data);
  // Reached only on success; a thrown error skips the ping and BullMQ retries
  await fetch("https://cronguard.app/api/ping/your-monitor-id");
});

If the migration dropped the schedule, if the queue is wedged, or if a bad scheduler id stopped the cadence, the ping stops arriving and you get paged. You do not have to work out which failure hit you, only notice that success went missing.

Frequently asked questions about BullMQ Job Schedulers

Does upgrading to BullMQ v6 break my existing repeatable jobs? Yes, if you still use the repeat option. BullMQ v6 removed the repeat option, the Repeat class, and the getRepeatableJobs, removeRepeatable, and removeRepeatableByKey methods. Code that still passes repeat compiles in JavaScript, but the option is ignored, so the job runs once and never repeats.

What replaces repeatable jobs in BullMQ v6? Job Schedulers, created with upsertJobScheduler. A scheduler holds a repeat rule and a job template, and produces jobs on cadence. Using upsert means redeploying the same scheduler id updates the existing schedule instead of creating a second one.

Why does my BullMQ scheduler stop producing jobs? A Job Scheduler keeps only one job in the delayed state and produces the next occurrence when the current one starts processing. If the previous job is stuck behind a backlog or left active by a crashed worker, the next run is never created and the schedule stalls silently.

Does the BullMQ cron pattern use the same syntax as crontab? Not exactly. BullMQ parses patterns with cron-parser, which accepts an optional seconds field as the first column. A five-field crontab expression works, but a six-field one is read with seconds first, so 0 15 3 * * * means 03:15:00 daily. Verify the next run time before trusting a copied expression.

How do I monitor recurring jobs in BullMQ? Watch the outcome from outside BullMQ, because its dashboard cannot show a job that was never produced. Have each worker report success only after the work completes, then alert when that report does not arrive on time. That single signal catches a schedule the migration dropped and one a stalled scheduler quietly stopped.

Further reading


Conclusion: BullMQ v6 did not deprecate repeatable jobs, it removed them, and the Job Scheduler that replaces them only helps if you migrate deliberately. The upgrade stays quiet about the gap: a leftover repeat option becomes a no-op, a congested queue starves the scheduler, and a timestamped scheduler id multiplies your runs. None of it shows on the queue's own dashboard. Watch for the absence of a successful run from outside, the one signal that catches every version of this failure.

Sources: BullMQ changelog, BullMQ Job Schedulers guide.

Share

Related posts

A fiber-optic patch panel in a rack, green connectors and yellow cables looping across rows of numbered ports
ReliabilityWhen a Scheduled Job Runs Long: Overlap, Skip, or Replace
Colorful syntax-highlighted source code on a black screen viewed at a slight angle, with a database query and functions like parseFloat and forEach visible in orange, blue and pink
ReliabilityThe Python Cron Parser Behind Airflow Is Not the Cron You Know
A person at a desk in a dark room, lit by two monitors filled with lines of code, looking toward the camera, in black and white
ReliabilityThe Kubernetes CronJob Deadlock That Stops Your Schedule For Good

Set up your first monitor.
It'll take 30 seconds.