Delivery lifecycle
For every event and consumer group, Pulse follows this lifecycle:Leases, heartbeats, and fencing
When a worker acquires an attempt, it writes:startedAt,lockedAt, andlockedUntil- a UUIDv7
lockOwner - a new UUIDv7
lockToken heartbeatAt
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:- Reap one expired attempt.
- Reconcile incomplete delivery state.
- Acquire existing work or discover new events.
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.
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
Useevent.id as the idempotency key sent to an external API:
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.