“Events are kept for 72 hours, so we can’t lose anything.” You’ll hear some version of that in most design reviews for Salesforce Platform Events and Change Data Capture, and it’s only partly true. Replay does recover a lot. It can’t recover everything, and the gaps sit in specific, predictable places. Below are the delivery guarantees Salesforce actually documents, the recovery tools on the publishing and subscribing sides, and the design choices that close what’s left. Everything is current to Summer ‘26.
If publishing and subscribing are new to you, the Platform Events deep dive covers the bus model and subscriber types. Whether events are the right tool in the first place is a separate question, answered in choosing a Salesforce integration pattern and the integration patterns overview.
What Salesforce actually guarantees
The official Integration Patterns guide is direct about three things:
- Platform events are published to the bus once. Salesforce doesn’t retry a publish on its side.
- In rare cases an event may not be persisted. The event bus is a distributed system without the guarantees of a transactional database. An event that isn’t persisted is never delivered and can’t be recovered.
- Events aren’t processed inside database transactions. Once published, an event can’t be rolled back.
So replay covers subscriber-side loss: the subscriber was offline or crashed. It can’t help with publish-side loss, because there is nothing stored to replay. A true “no message may be lost” requirement needs reconciliation between the systems or a durable queue in middleware.
Retention
High-volume Platform Events and Change Data Capture events are stored for 72 hours. Legacy standard-volume events (defined before Spring ‘19) keep a 24-hour window and are being retired; check the current release notes for the retirement date if you still have any. Salesforce doesn’t guarantee storage beyond the retention period, even though purging sometimes runs late.
Where notifications get lost
| Loss point | What happens | Mitigation |
|---|---|---|
| Publish fails | The event never reaches the bus | Check the Database.SaveResult returned by EventBus.publish(). Replay can’t help |
| Transaction rolls back after the event went out | Subscribers hear about a change that never happened | Use Publish After Commit (the default) so the event fires only when the save succeeds |
| Event not persisted | Rare, and unrecoverable | Reconciliation job or a middleware queue |
| Subscriber offline | Events pile up on the bus | Store the last replay ID and resume from it, within 72 hours |
| Subscriber offline for more than 72 hours | Events have aged out | A job that compares both systems and repairs the gaps |
| Same event delivered twice | Duplicate records or double counting | Make the subscriber idempotent |
| Subscriber saves its position too early | It records the first event in a batch as done and skips the rest | Process the whole batch, then store the replay ID of the last event handled |
| Apex subscriber trigger throws | The platform doesn’t retry automatically | Opt in with EventBus.RetryableException or checkpoints |
Duplicates are normal
Salesforce can deliver the same event more than once. Design every subscriber so that handling an event twice is harmless: upsert on an external ID instead of inserting, and track what you have already processed.
To identify an event message uniquely, use the EventUuid field. The replay ID marks a position in the stream and isn’t meant to serve as an identity key.
Recovering inside an Apex subscriber
Apex subscriber triggers don’t retry on uncaught exceptions. You have two opt-in tools, and the difference comes up in interviews.
EventBus.RetryableException | setResumeCheckpoint(replayId) | |
|---|---|---|
| Current execution | Stops | Continues until the failure |
| DML done before the failure | Rolled back | Committed |
| Next run | The whole batch is resent after a delay that grows with each retry | Starts with the event after the checkpoint |
| Best for | Temporary problems likely to clear, such as a locked record or an unavailable dependency | A limit or exception partway through a batch, after some events already succeeded |
Retrying the whole batch when a dependency isn’t ready yet:
trigger OrderEventTrigger on Order_Event__e (after insert) {
Integer maxRetries = 5;
if (!OrderEventHandler.dependencyReady()) {
if (EventBus.TriggerContext.currentContext().retries < maxRetries) {
throw new EventBus.RetryableException('Dependency not ready, retrying.');
}
ErrorLog.recordBatch(Trigger.new); // final attempt: keep the evidence
return;
}
OrderEventHandler.processAll(Trigger.new);
}
Checkpointing as you go, so a failure halfway through doesn’t repeat finished work:
trigger OrderEventTrigger on Order_Event__e (after insert) {
EventBus.TriggerContext ctx = EventBus.TriggerContext.currentContext();
for (Order_Event__e evt : Trigger.new) {
OrderEventHandler.process(evt);
ctx.setResumeCheckpoint(evt.ReplayId); // next run starts after this event
}
}
Pick one approach per trigger. Retries are capped, so the final attempt always needs a fallback that logs the failure somewhere a person will see it. And never publish the same event type from its own trigger, because that creates an infinite loop.
Replay options
When a subscriber connects, it tells Salesforce where to start.
| Option | Behaviour | When to use it |
|---|---|---|
-1 | Only events published after subscribing | The recommended default |
-2 | Every event in the retention window, then new ones | Catching up after a connection failure |
| A stored replay ID | Events after that position | Resuming exactly where you stopped |
The Pub/Sub API uses an enum for the same idea: LATEST, EARLIEST, or CUSTOM with a replay ID.
Salesforce warns against leaning on -2. When many events are stored, subscribing from the start of the window can slow things down noticeably.
Replay IDs aren’t a counter
Replay IDs aren’t guaranteed to be contiguous. Event 110 can follow event 101 with nothing missing in between. So:
- Never compute a replay ID, such as
lastId + 1. - Never read a gap as lost data.
- Store the last value you processed and pass it back exactly as you received it.
CometD vs Pub/Sub API
Platform Events and CDC describe what gets published. CometD and the Pub/Sub API are two ways to subscribe to the same bus.
| CometD (Streaming API) | Pub/Sub API | |
|---|---|---|
| Transport | HTTP/1.1 long polling | HTTP/2 with gRPC |
| Direction | Subscribe only | Publish and subscribe |
| Payload format | JSON | Avro with a versioned schema |
| Flow control | None | The client sets numRequested |
changedFields in CDC | A plain list | A bitmap you decode |
| Investment | Supported, no new features | The recommended option for new work |
Why flow control matters
Imagine a Data Loader job that updates 5,000 records and fires 5,000 CDC events. A CometD subscriber gets all of them as fast as Salesforce can push them, with no way to slow down. A Pub/Sub API subscriber can ask for 10 at a time, and Salesforce holds the rest until it asks again.
This matters most after downtime, when the backlog is at its largest. Set numRequested in your client code, in the FetchRequest. There is no Setup option for it.
Where CometD still wins
In the browser. The lightning/empApi module for LWC and Aura uses CometD, and there is no gRPC option for components. For in-app notifications, CometD remains the practical choice.
Designing for “no data loss”
When a requirement says messages must never be lost, a sound answer combines several layers:
- Publish After Commit, and check every
SaveResult. - Durable replay state: store the last processed replay ID outside the subscriber’s memory.
- Idempotent processing keyed on
EventUuidor a business key. - Checkpoints or retries for Apex subscribers, with a logged final failure.
- Reconciliation that compares source and target on a schedule, covering outages longer than 72 hours and the rare event that never persisted.
- A middleware queue when the business can’t tolerate even that small gap.
The retry and idempotency patterns behind steps 3 and 4 are covered in the integration resilience guide.
When someone asks you “can we lose events?”, whether in a design review or an interview, the short answer is that replay covers subscriber-side loss only. Salesforce states that an event can occasionally fail to persist and can’t be recovered, so a strict no-loss requirement needs reconciliation or a queue in the middleware layer. That answer shows you have read the documented limits closely.