Platform Events are Salesforce’s answer to the question “how do two systems talk without knowing about each other.” They underpin event-driven architecture on the platform — decoupling producers from consumers across Apex, Flow, and external systems through a single asynchronous bus. This deep dive covers how the bus actually behaves: publish semantics, every subscriber type, replay and durability, retry, and the delivery allocations that surprise teams at scale. Current to Summer ‘26 and API v67.0.
Platform Events sit alongside the other patterns in the Salesforce integration patterns guide — reach for them when you need fan-out and decoupling, not when you need a synchronous answer.
The event bus model
A Platform Event is a special sObject (suffix __e) representing a message. Publishers place events on the bus; subscribers read them asynchronously. Neither side holds a reference to the other — you can add a third subscriber years later without touching the publisher. That decoupling is the entire value proposition, and also the source of every “but where did my event go” debugging session.
Two volume tiers exist: high-volume events (the default and correct choice) are optimized for throughput and stored efficiently; standard-volume is legacy. Define events in Setup with a clear field schema — the payload is the contract every subscriber depends on, so version it as carefully as any API.
Publishing — and the after-commit decision
You publish with EventBus.publish:
List<Order_Event__e> events = new List<Order_Event__e>();
events.add(new Order_Event__e(
Order_Id__c = order.Id,
Status__c = 'Shipped',
Amount__c = order.TotalAmount
));
List<Database.SaveResult> results = EventBus.publish(events);
The pivotal design choice is the publish behavior, set on the event definition:
- Publish After Commit — the event fires only if the surrounding transaction commits. If the transaction rolls back, the event never reaches the bus. This is the safe default whenever the event asserts something about persisted data (“Order shipped” should not fire if the order save failed).
- Publish Immediately — the event fires the moment
publishruns, independent of the transaction outcome. Correct for audit logs and telemetry that must survive even a failed transaction — you want the “attempt happened” signal regardless.
Choosing After Commit for telemetry loses the events you most wanted on failure; choosing Immediately for a “record created” event leaks events for records that never saved. The wrong default here is a class of bug that only shows up under rollback conditions, which is to say, in production.
Always inspect the SaveResult — publishing can fail, and an unchecked failure is a silently dropped message.
Subscribers — three ways to consume
Apex trigger (after-insert on the event). The in-platform workhorse:
trigger OrderEventTrigger on Order_Event__e (after insert) {
for (Order_Event__e evt : Trigger.new) {
// Runs async as Automated Process user.
// Batch of up to 2,000 events per execution.
}
}
Two things bite here. First, it runs as the Automated Process user, not the publishing user — so running user context, ownership, and the “Set Created By and Last Modified By” org setting all matter for anything the trigger writes. Second, batches reach 2,000 events, so the same bulkification discipline as any trigger applies, multiplied.
Flow (platform-event-triggered flow). A declarative subscriber, ideal when the reaction is record updates or notifications without code. It processes one event per run and shares the same async, Automated-Process execution model.
Pub/Sub API (external subscribers). The modern external transport: gRPC over HTTP/2 with binary Avro payloads, superseding the older CometD/empApi streaming. External systems subscribe, receive events in near-real-time, and track their position by ReplayId. More efficient and more robust than CometD for anything outside the platform.
Replay and the 72-hour wall
Every event gets a ReplayId marking its position on the bus. External subscribers store the last ReplayId they processed; on reconnect they resume from there, recovering anything missed during a disconnect. The hard constraint: events are retained for 72 hours. Replay works only inside that window — a subscriber down for a long weekend can miss events permanently. Design external consumers to persist their ReplayId durably and to alert when the gap between now and their last ReplayId approaches the retention edge.
This is also why Platform Events are not a database: they are a transient transport. If a consumer needs the full history, it must persist what it consumes; the bus will not hold it.
Retry — bounded, and you own the final attempt
When an Apex subscriber hits a transient failure, signal a retry:
trigger OrderEventTrigger on Order_Event__e (after insert) {
try {
processEvents(Trigger.new);
} catch (CalloutException e) {
if (EventBus.TriggerContext.currentContext().retries < 4) {
throw new EventBus.RetryableException('Transient — retry');
} else {
logFailure(Trigger.new, e); // final attempt: persist, don't lose
}
}
}
EventBus.RetryableException asks the platform to redeliver the batch. Retries are capped — by a retry count and by roughly a 10-minute elapsed ceiling. After that the platform gives up, so the trigger must check EventBus.TriggerContext...retries and, on the last attempt, fall back to an error log rather than throwing into the void. A retry strategy without a terminal branch is just delayed data loss.
Delivery allocations — the limits that bite at scale
Platform Events draw on daily allocations separate from API limits, and they are easy to blow through under fan-out:
- Published events per 24 hours — bounded by your org’s allocation and add-on licenses
- Delivered events to CometD/empApi and Pub/Sub subscribers, counted per subscriber — three subscribers to one event is three deliveries against the allocation
- Apex-trigger and flow delivery do not count against the same external delivery allocation, but do consume async processing capacity
The fan-out arithmetic is the trap: one published event delivered to four external subscribers is four counts. Architectures that look fine at 1,000 events/day quietly fail at 100,000 because the delivery multiplier was never modeled. Estimate published × subscriber-count before committing to an event-driven design, not after the allocation error fires.
When Platform Events are the wrong tool
- You need a synchronous answer — events are fire-and-forget; use a REST callout.
- You need guaranteed long-term durability — 72 hours is a transport window, not storage. Persist on consumption.
- Strict ordering across partitions matters — ordering holds within a publish but is not a cross-stream guarantee; design idempotent consumers instead of assuming order.
- Tiny, tightly-coupled, same-transaction logic — if a trigger can just call a handler synchronously, an event adds latency and an allocation cost for no decoupling benefit.
Used where decoupling and fan-out genuinely pay off — and modeled honestly for the 72-hour window and the per-subscriber delivery math — Platform Events are the backbone of event-driven Salesforce. Used as a generic message queue or a database, they become an expensive way to lose data 72 hours at a time.
Test your knowledge — Integration
10 questions · Basic to Advanced