Skip to content

NATS

NatsBackend makes sure one instance runs each scheduled time, using NATS JetStream.

import { NatsBackend, Scheduler } from 'anarchon';
const scheduler = new Scheduler({
backend: new NatsBackend({ servers: 'nats://nats.internal:4222' }),
});

JetStream must be enabled on the server (nats-server -js).

Option Default Meaning
servers 'nats://localhost:4222' One URL or a list
connection Your own connection from @nats-io/transport-node. You keep ownership
stream 'CRON' JetStream stream name
prefix 'cron.' Subject prefix
duplicateWindowMs 300000 (5 min) How long JetStream remembers a claimed run
historySize 1000 About how many runs to keep per job, at least
replicas 1 for a new stream Stream replicas in a NATS cluster
import { connect } from '@nats-io/transport-node';
const connection = await connect({ servers: ['nats://n1:4222', 'nats://n2:4222', 'nats://n3:4222'] });
new NatsBackend({ connection, stream: 'BILLING_CRON', prefix: 'billing.cron.', replicas: 3 });

The feature used is JetStream’s message de-duplication. A publisher can attach a message ID; within the stream’s duplicate window, JetStream stores only the first message with a given ID.

sequenceDiagram
  participant A as Instance A
  participant B as Instance B
  participant J as JetStream stream CRON
  A->>J: publish cron.sync.claims<br/>Nats-Msg-Id: sync:1791633610000
  J-->>A: ack (seq 812, duplicate: false)
  Note over A: runs the job
  B->>J: publish cron.sync.claims<br/>Nats-Msg-Id: sync:1791633610000
  J-->>B: ack (seq 812, duplicate: true)
  Note over B: skips the run
  1. Every instance publishes its claim to <prefix><job>.claims with the header Nats-Msg-Id: <job>:<slot>. The ID is the same on every instance, because the slot is.
  2. The stream stores the first message with that ID. For every later one it answers duplicate: true and stores nothing.
  3. An instance won if its acknowledgement is not a duplicate. The stream (its leader, in a cluster) orders the publishes, so exactly one is first.

If an acknowledgement says “duplicate”, the instance reads the stored message and compares tokens. That way a claim that the client itself retried is still recognized as its own.

anarchon creates the stream on start() if it doesn’t exist, covering the subjects <prefix>>. If a stream with that name already exists with other settings, anarchon updates the duplicate window and per-subject limit and adds its subjects.

Subject Holds
<prefix><job>.claims One message per claimed run, and per manual run
<prefix><job>.results One message per finished run
  • jitterMs + maxLateMs must be shorter than duplicateWindowMs. Outside the window JetStream no longer recognizes a repeat, and a very late instance could run the job again. anarchon checks this when you add a job. The defaults (2 s + 60 s against 5 min) are fine.
  • Job names may only contain letters, digits, _ and -, because they become subjects.
  • Replicas. In a NATS cluster set replicas: 3 so the stream, and its memory of claimed runs, survives the loss of a server. When replicas is set, an existing stream is updated to it; when it isn’t, an existing stream keeps the replicas it has.

history() reads the job’s claims and results from the stream and joins them. lastRun() and runNow() work as on other backends. A run whose runner died keeps the status running. History is trimmed by count only, so historyTtlMs is refused.