Skip to content

anarchon

Leaderless cron and interval scheduling for Node.js services. Define the same jobs in every instance; an atomic claim in the store they share selects one runner for each run.

npm version npm downloads node license

import { RedisBackend, Scheduler } from 'anarchon';
const scheduler = new Scheduler({ backend: new RedisBackend({ redis: 'redis://localhost:6379' }) });
scheduler
.every('sync', '10s', async (ctx) => { /* ... */ })
.cron('digest', '0 8 * * 1-5', async (ctx) => { /* ... */ }, { timezone: 'Europe/Athens' });
await scheduler.start();

Run that code in three instances, and one instance runs “sync” every 10 seconds, not all three.

flowchart LR
  subgraph service["Your service, 3 instances"]
    A["Instance A"]
    B["Instance B"]
    C["Instance C"]
  end
  S[("Redis, Valkey,<br/>PostgreSQL or SQLite")]
  A -- "claim the 12:00:10 run" --> S
  B -- "claim the 12:00:10 run" --> S
  C -- "claim the 12:00:10 run" --> S
  S -- "won" --> B
  S -. "taken" .-> A
  S -. "taken" .-> C

No leader election

Nothing is elected. One atomic call to the store decides who runs each time, and a random delay spreads the work across instances.

Recovery after a runner fails

On Redis, Valkey, PostgreSQL and SQLite the winner renews a lease while the job runs. If it dies or freezes, another instance starts the run again as a new attempt, so handlers must tolerate retries. RabbitMQ and NATS only select the winner.

Long jobs don't pile up

On lease backends, the next run of a job is skipped while the current one holds a live lease. A frozen runner can briefly overlap the instance that replaced it.

Catch-up after downtime

On lease backends, the latest run that was due while every instance was down runs when the first one comes back, if it was due within catchUpMs (5 minutes by default). Older runs are not replayed.

anarchon does not guarantee exactly-once side effects. Guarantees and limits states what is promised on each backend.