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.
Calling close() stops acquisition of new work but keeps heartbeats active for callbacks already
running. Pulse waits for those callbacks to settle before closing MongoDB, preventing another
replica from reaping intentionally draining work during graceful shutdown.
A handler may initiate and await close() without deadlocking itself. Pulse starts the graceful
close and lets that handler return so its attempt can be acknowledged. Lifecycle code outside the
handler can call close() again to await the shared close promise.
Recovery after a crash
The process coordinator runs three recovery steps before looking for new events:- Reap a bounded batch of expired attempts.
- Reconcile a bounded batch of 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.
- A terminal delivery was written but its history and delivery TTL dates were not.
consumerGroup + eventId and deliveryId + attempt ensure replicas converge on one logical record.
Concurrent publication ordering
Pulse assigns every new event an internal BSON timestamp in the same MongoDB write that creates the event. Discovery advances through that MongoDB-assigned sequence instead of relying on application clocks or the order in which concurrentpublish() promises resolve.
This prevents a slow concurrent insert from appearing behind an already-advanced cursor. The
public createdAt field remains available as the application timestamp. Ordered consumers execute
new events by the same MongoDB sequence; legacy events without a sequence fall back to
createdAt + eventId.
Events created by older Pulse versions do not have the internal sequence. Upgraded consumers keep
an independent createdAt + _id cursor for those legacy events and check it before the
MongoDB-assigned cursor, so old and new publishers can coexist during a rolling upgrade.
Subscriptions first created before Pulse 4.4.3 may have only the legacy cursor. Their first current
consumer safely scans the retained sequenced backlog and relies on the delivery unique key to avoid
re-execution. Keep historyRetentionMs at least as long as eventRetentionMs until every durable
subscription has a cursorSequence; otherwise an event whose old delivery record already expired
can be delivered again, consistent with Pulse’s at-least-once guarantee.
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:
MongoDB connection capacity
Pulse configuresmaxPoolSize: 5 by default. With 500 service replicas, that bounds the application
pool for the active MongoDB server to 2,500 connections instead of the driver’s default ceiling of
50,000. Pools start empty because minPoolSize remains 0 and grow only when concurrent operations
need them.
The limit is per MongoClient and per server in the MongoDB topology. The driver also keeps
monitoring connections outside the application pool. If all five connections are busy, operations
wait for one to return rather than opening beyond the configured maximum.
Tune the limit per service when needed:
Polling
Polling and reconciliation are Pulse’s only discovery, acquisition, and recovery mechanisms. Pulse does not open MongoDB Change Streams. Every process has one coordinator regardless ofworkerCount. That coordinator performs one acquisition query across every local topic per poll,
instead of one query per topic. Candidate topics rotate in bounded batches, so a busy topic cannot
permanently hide another one. Use pollIntervalMs to balance idle query volume against delivery latency.
Since Pulse 4.5.2, handler slots never poll MongoDB independently.
workerCount only controls how
many callbacks may execute concurrently; increasing it does not multiply the process’s discovery,
acquisition, or recovery polling loops.consumerGroup + topic, configured with
discoveryLockTimeoutMs and defaulting to 10 seconds. A process acquires and renews all of its topic
leases in bulk, then discovers its owned topics in one aggregate query. Lease and event polling thus
scale with processes rather than processes multiplied by topics, while a replica that registers a
different topic set can still own and recover those topics safely.
Cursor updates are fenced by the same topic lease token. If a query finishes after its lease was
lost, the stale reader may create idempotent delivery records but cannot advance the cursor. The new
owner safely repeats discovery. Temporary query errors do not voluntarily abandon a lease; expiry
still permits failover.
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.
Errored attempts that still have a retry do not receive an expiry date. This preserves the retry
number and recovery evidence even when the retry delay or a complete service outage lasts longer
than historyRetentionMs. Once the delivery becomes terminal, Pulse assigns the same terminal
retention deadline to all its completed attempts, then makes the delivery expirable. If a machine
stops between those writes, the non-expiring terminal delivery remains as a reconciliation marker.
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, TTL, sparse, hidden, partial-filter, or collation configuration.