Integration

Can You Lose Salesforce Platform Events? Replay IDs, Retries and Delivery Guarantees Explained

By Rishabh Panwar · 7 min read · Advanced

“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 pointWhat happensMitigation
Publish failsThe event never reaches the busCheck the Database.SaveResult returned by EventBus.publish(). Replay can’t help
Transaction rolls back after the event went outSubscribers hear about a change that never happenedUse Publish After Commit (the default) so the event fires only when the save succeeds
Event not persistedRare, and unrecoverableReconciliation job or a middleware queue
Subscriber offlineEvents pile up on the busStore the last replay ID and resume from it, within 72 hours
Subscriber offline for more than 72 hoursEvents have aged outA job that compares both systems and repairs the gaps
Same event delivered twiceDuplicate records or double countingMake the subscriber idempotent
Subscriber saves its position too earlyIt records the first event in a batch as done and skips the restProcess the whole batch, then store the replay ID of the last event handled
Apex subscriber trigger throwsThe platform doesn’t retry automaticallyOpt 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.RetryableExceptionsetResumeCheckpoint(replayId)
Current executionStopsContinues until the failure
DML done before the failureRolled backCommitted
Next runThe whole batch is resent after a delay that grows with each retryStarts with the event after the checkpoint
Best forTemporary problems likely to clear, such as a locked record or an unavailable dependencyA 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.

OptionBehaviourWhen to use it
-1Only events published after subscribingThe recommended default
-2Every event in the retention window, then new onesCatching up after a connection failure
A stored replay IDEvents after that positionResuming 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
TransportHTTP/1.1 long pollingHTTP/2 with gRPC
DirectionSubscribe onlyPublish and subscribe
Payload formatJSONAvro with a versioned schema
Flow controlNoneThe client sets numRequested
changedFields in CDCA plain listA bitmap you decode
InvestmentSupported, no new featuresThe 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:

  1. Publish After Commit, and check every SaveResult.
  2. Durable replay state: store the last processed replay ID outside the subscriber’s memory.
  3. Idempotent processing keyed on EventUuid or a business key.
  4. Checkpoints or retries for Apex subscribers, with a logged final failure.
  5. Reconciliation that compares source and target on a schedule, covering outages longer than 72 hours and the rare event that never persisted.
  6. 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.

Frequently asked questions

Are Salesforce Platform Events guaranteed to be delivered?

No. Salesforce publishes each event to the bus once with no retry on its side, and the Integration Patterns guide states that in rare cases an event may not be persisted and can't be recovered. Replay only recovers events that reached the bus.

Can a Platform Event be delivered more than once?

Yes. Subscribers must be idempotent, meaning that processing the same event twice leaves the same result as processing it once. Use the EventUuid field, which uniquely identifies an event message, to detect repeats.

What is the difference between EventBus.RetryableException and setResumeCheckpoint?

RetryableException stops the trigger, rolls back its DML and asks the platform to resend the whole batch later. setResumeCheckpoint marks the last event processed successfully; work done before the failure stays committed and the next run starts after the checkpoint.

What do replay IDs -1 and -2 mean?

-1 subscribes to new events only and is the recommended default. -2 delivers every event still in the retention window plus new ones; Salesforce advises using it sparingly because it can slow performance when many events are stored.

Are replay IDs sequential?

No. Replay IDs mark position in the stream but aren't guaranteed to be contiguous. Never compute a replay ID or treat a gap as lost events; store the last value you processed and pass it back unchanged.

Why is the Pub/Sub API better than CometD for high-volume subscribers?

Pub/Sub API supports flow control: the client asks for a set number of events at a time and Salesforce holds the rest. CometD has no flow control, so a burst of events arrives all at once.