Skip to content

Overlap Prevention

The scheduler always waits for a tick’s jobs to finish before scheduling the next tick — so the scheduler’s own loop never starts a second copy of a job while the first is still running. Overlap only becomes possible when the job’s callback is invoked from somewhere outside that loop: a manual job.run() for a forced re-run, a boot-time recovery sweep, a separate scheduler instance pointed at the same job, etc.

preventOverlap() is how you tell the scheduler “this job has external invocation paths — if a tick fires while one of those is still running, skip the tick rather than try to run again.” The scheduler then emits a job:skip event so you can observe the gap in your logs or metrics.

import { scheduler, job } from "@warlock.js/scheduler";
scheduler.addJob(
job("process-queue", async () => {
// This might take several minutes
await queue.processAllPendingItems();
})
.everyMinutes(5)
.preventOverlap() // skip the 5-min tick if a previous run is still going
);
scheduler.start();

When a tick finds the job already running, the scheduler emits a job:skip event with the reason "Job is already running", then moves on.

A common shape — startup recovery + normal scheduling on the same callback:

import { scheduler, job } from "@warlock.js/scheduler";
const queueJob = job("process-queue", processQueueOnce)
.everyMinutes(5)
.preventOverlap();
scheduler.addJob(queueJob);
// Boot-time sweep — kick the job off immediately, don't wait for the first tick.
queueJob.run().catch(error => log.error({ error }, "boot sweep failed"));
scheduler.start();

If the boot sweep takes longer than 5 minutes, the first scheduled tick will land mid-sweep. With preventOverlap() set, the tick emits job:skip and the sweep finishes uninterrupted.

import { scheduler } from "@warlock.js/scheduler";
scheduler.on("job:skip", (name, reason) => {
// name = "process-queue"
// reason = "Job is already running"
console.log(`[scheduler] ${name} skipped — ${reason}`);
});

Pass false to explicitly re-enable concurrent execution if you’ve previously set it:

job("task", fn).everyMinute().preventOverlap(false);
job.preventOverlap(skip?: boolean): this
// skip defaults to true
import { scheduler } from "@warlock.js/scheduler";
const j = scheduler.getJob("process-queue");
if (j?.isRunning) {
console.log("Job is currently executing — will be skipped on next tick");
}

During scheduler.shutdown(), in-flight jobs are always allowed to finish (up to the configured timeout), regardless of whether overlap prevention is enabled. This is separate from the tick-time skip behavior.

// Give jobs up to 60 s to finish during shutdown
await scheduler.shutdown(60_000);

See Configuration for more on graceful shutdown.

preventOverlap() only guards a single process. When several app instances run the same schedule, add onOneServer() so exactly one of them runs each tick:

import { scheduler, job } from "@warlock.js/scheduler";
scheduler.addJob(job("nightly-report", buildReport).daily().at("02:00").onOneServer());
scheduler.addJob(
job("sync", sync).everyMinutes(5).onOneServer({ lockTtl: "10m", key: "sync-v2" }),
);

For every tick, the servers race to create a cache entry keyed scheduler.<key ?? name>.<scheduledTickEpochMs>. The server that creates it runs the job; the others skip that tick and emit job:skip.

That entry is a claim, not a lock: it is create-only, has a TTL, and is never released. Because it is never released, a server whose timer fires late — after the winner already finished — still loses, so a tick never runs twice.

| Option | Default | Purpose | | --- | --- | --- | | lockTtl | min(interval, 1h), 60s floor (1h for cron jobs) | How long the claim lives | | key | the job name | Stable key shared by every server |

  • A shared cache driver (redis or pg). The memory driver is per-process, so it only dedupes within one process.
  • @warlock.js/cache is an optional peer dependency — install it when you use onOneServer().
  • A job name or a key. onOneServer() throws if it has neither, because the claim key must be identical on every server.