Skip to main content
Pulse uses MongoDB documents, unique indexes, renewable leases, and reconciliation to provide recoverable delivery without requiring multi-document transactions.

Delivery lifecycle

For every event and consumer group, Pulse follows this lifecycle:
The history attempt exists before the handler starts. Operators can therefore distinguish work that is queued, currently running, or abandoned by a dead worker.

Leases, heartbeats, and fencing

When a worker acquires an attempt, it writes:
  • startedAt, lockedAt, and lockedUntil
  • a UUIDv7 lockOwner
  • a new UUIDv7 lockToken
  • heartbeatAt
Pulse renews the attempt lease approximately every third of lockTimeoutMs. Ordered consumers also renew the topic ordering lease. Every success or error update includes the fencing token in its MongoDB filter. If another replica has already recovered the attempt, the stale worker’s token no longer matches and it cannot overwrite the new result. Choose a lockTimeoutMs that comfortably exceeds expected event-loop stalls and temporary MongoDB connectivity interruptions. Heartbeats keep normal long-running asynchronous handlers alive, but blocking the JavaScript event loop can prevent renewal.

Recovery after a crash

Every worker loop runs three recovery steps before looking for new events:
  1. Reap one expired attempt.
  2. Reconcile incomplete delivery state.
  3. Acquire existing work or discover new events.
An expired attempt is atomically changed from pending to error with code worker_lost. At-least-once mode then creates the next numbered attempt; at-most-once mode makes the delivery terminal. Reconciliation repairs safe partial states idempotently:
  • Delivery exists but its first history attempt does not.
  • History is successful but delivery is still pending.
  • History is errored but its retry was not created.
  • Event discovery was interrupted before the subscription cursor advanced.
Unique indexes on consumerGroup + eventId and deliveryId + attempt ensure replicas converge on one logical record.

Crash windows

At-least-once delivery cannot prove whether an external side effect happened before a machine disappeared. Exactly-once side effects require idempotency in the system receiving the side effect.

Design idempotent handlers

Use event.id as the idempotency key sent to an external API:
When the side effect is in MongoDB, store the event ID with a unique index in the same transaction as the domain write:
Treat a duplicate-key error on processedEvents._id as an already completed handler. Do not acknowledge the idempotency marker before a non-transactional side effect that can still fail.

Error codes

Pulse writes these built-in error codes to history: Internal errors and lost-fencing notifications are delivered to onError. The callback should report or log quickly and must not throw:

Polling and Change Streams

Polling and reconciliation guarantee progress. Change Streams only wake sleeping workers after new event inserts. If an active Change Stream fails, Pulse reports the error and attempts to open it again after pollIntervalMs. Missing a notification does not lose the event.

Collections and retention

With the default prefix, Pulse owns: Retention uses absolute expiresAt dates and TTL indexes with expireAfterSeconds: 0. When retention is null, Pulse omits expiresAt; MongoDB keeps those documents even though the TTL index remains.

Startup safety

During initialization, Pulse creates missing collections and named indexes, then inspects their keys and relevant options. Startup fails before workers begin when:
  • MongoDB permissions do not allow the required setup.
  • Existing data violates a required unique index.
  • A named Pulse index has incompatible keys, uniqueness, or TTL configuration.
  • changeStreams: 'required' is used on an unsupported topology.
Pulse never drops or replaces an incompatible index automatically. The error identifies the collection, index name, expected configuration, and actual configuration so an operator can make the migration explicitly. See the API reference for the exact index names and configuration defaults.