Skip to content
Warlock.js v4

Retry & Backoff

Jobs can automatically retry on failure. Configure this with .retry() — optionally enabling exponential backoff so successive attempts wait progressively longer.

import { scheduler, job } from "@warlock.js/scheduler";
scheduler.addJob(
job("send-report", async () => {
await emailService.send(report);
})
.daily()
.at("08:00")
.retry(3) // retry up to 3 times, 1 s between attempts (default delay)
);

By default the delay between retries is 1000 ms (1 second).

Pass the delay in milliseconds as the second argument:

import { scheduler, job } from "@warlock.js/scheduler";
scheduler.addJob(
job("sync-inventory", async () => {
await externalApi.syncInventory();
})
.everyHour()
.retry(5, 2000) // retry up to 5 times, wait 2 s between each attempt
);

Pass a backoffMultiplier as the third argument. Each retry multiplies the base delay by the multiplier raised to the attempt number:

import { scheduler, job } from "@warlock.js/scheduler";
scheduler.addJob(
job("process-queue", async () => {
await queue.processNext();
})
.everyMinutes(10)
.retry(5, 1000, 2) // 1s → 2s → 4s → 8s → 16s
);

Delay schedule with retry(5, 1000, 2):

AttemptWait before attempt
1 (initial)
21 000 ms
32 000 ms
44 000 ms
58 000 ms
616 000 ms

The formula is: delay × backoffMultiplier ^ (attempt - 1)

job.retry(
maxRetries: number,
delay?: number, // milliseconds, default 1000
backoffMultiplier?: number // optional, enables exponential backoff
): this

After a job completes (successfully or not), the JobResult object includes how many retries were made:

import { scheduler } from "@warlock.js/scheduler";
scheduler.on("job:complete", (name, result) => {
if (result.retries && result.retries > 0) {
console.log(`${name} succeeded after ${result.retries} retries`);
}
});
scheduler.on("job:error", (name, error) => {
// Fires after ALL retries are exhausted
console.error(`${name} failed permanently:`, error);
});

See Events for the full event reference.

What Happens After All Retries Are Exhausted

Section titled “What Happens After All Retries Are Exhausted”

When a job exhausts every retry and the final attempt still throws, the scheduler:

  1. Emits job:error once with the final error.
  2. Advances nextRun by the job’s configured interval — exactly the same as a successful run.

The job will fire again at its next scheduled slot. It does not re-fire on every tick, and the retry count resets for the next run.

// Fires every 10 minutes regardless of whether each attempt succeeds.
// If the 10:00 run exhausts all retries, the next run is at 10:10.
job("process-queue", queue.processNext)
.everyMinutes(10)
.retry(3, 1000);

If you need a permanently-failing job to stop firing, listen for repeated job:error events and call scheduler.removeJob(name) after N failures — see Events for the listener shape.

ScenarioRecommended config
Transient network errorsretry(3, 1000)
Rate-limited external APIretry(5, 2000, 2)
Database deadlocksretry(3, 500)
Long-running payment gatewayretry(4, 5000, 1.5)