Skip to content
ForceTricks
Back to blog

Platform Events in Complex Orgs: Beyond Basic Pub/Sub

7 min read
SeriesEvent-Driven SalesforcePart 1 of 1
  1. 1Platform Events in Complex Orgs: Beyond Basic Pub/Sub

The docs make Platform Events look straightforward: publish an event, subscribe with a trigger, done. That model works fine for a simple notification pattern — one publisher, one consumer, low volume, failures are acceptable.

Complex orgs are none of those things.

The Replay Buffer Is Not a Recovery Strategy

The 72-hour replay window is commonly misunderstood as "built-in retry." It isn't. It's an ordered log of events that a subscriber can rewind using a ReplayId.

Here's what the window actually means operationally:

  • Events are retained for 72 hours on the event bus
  • Each event gets a monotonically increasing ReplayId — this is not a UUID, it's a sequence number per channel
  • A subscriber can request events from -2 (all retained), -1 (new only), or a specific ReplayId

The operational implication: if your Apex trigger subscriber crashes and you need to replay, you replay from Setup... except you can't. There is no UI to replay events. Replay is only available via the Streaming API (CometD client) or the Pub/Sub API. If your consumer is an Apex trigger, you cannot replay missed events into it — you'd need to build a separate CometD subscriber to re-inject them.

Idempotency Is Non-Negotiable

Any consumer that receives events with -2 replay (or any point-in-time rewind) will reprocess events it already handled. This is not a corner case — it happens every time a subscriber reconnects after a long gap.

Design every consumer to be idempotent from the start. The standard pattern:

trigger OrderEventTrigger on Order_Event__e (after insert) {
    Set<String> correlationIds = new Set<String>();
    for (Order_Event__e evt : Trigger.new) {
        correlationIds.add(evt.Correlation_Id__c);
    }

    // Check which correlationIds have already been processed
    Map<String, ProcessedEvent__c> existing = new Map<String, ProcessedEvent__c>();
    for (ProcessedEvent__c pe : [
        SELECT Correlation_Id__c FROM ProcessedEvent__c
        WHERE Correlation_Id__c IN :correlationIds
    ]) {
        existing.put(pe.Correlation_Id__c, pe);
    }

    List<ProcessedEvent__c> toInsert = new List<ProcessedEvent__c>();
    for (Order_Event__e evt : Trigger.new) {
        if (existing.containsKey(evt.Correlation_Id__c)) {
            continue; // already processed, skip
        }
        // process the event...
        toInsert.add(new ProcessedEvent__c(
            Correlation_Id__c = evt.Correlation_Id__c
        ));
    }

    insert toInsert;
}

The Correlation_Id__c field on the event should be set by the publisher — a UUID or a deterministic hash of the business key. Never rely on the ReplayId for idempotency; it changes if the event is re-published.

Ordering: Platform Events Have No Guarantee

This is the one that catches architects who come from messaging systems like Kafka or SQS FIFO. Platform Events do not guarantee delivery order within a channel. Events published in sequence can arrive out of order at the subscriber.

In practice, ordering is usually preserved for low-volume flows — but "usually" is not a contract, and it breaks under load.

Design Patterns When Order Matters

Sequence numbers in the event payload. The publisher stamps each event with a monotonic sequence number scoped to a business key. The consumer buffers out-of-order events and processes them in sequence:

// Publisher stamps the sequence
Integer nextSeq = getNextSequence(orderId); // maintained in a custom object or Redis
EventBus.publish(new Order_Event__e(
    Order_Id__c    = orderId,
    Sequence__c    = nextSeq,
    Payload__c     = JSON.serialize(changeData)
));

The consumer side becomes complex fast — buffering unordered events in Apex is painful, especially under the 5-minute governor limit. This pattern works better when the consumer is an external system.

Event envelope with conflict resolution. Instead of processing events in order, design the payload to be self-contained enough that any order produces the correct final state. The last-write-wins pattern using a Last_Modified__c timestamp in the payload is the simplest version.

Use Change Data Capture when strict ordering is required. CDC events for a given record arrive in transaction order. If you're tracking sequential state transitions on a standard object, CDC is a better fit than Platform Events precisely because you don't control the publisher.

High-Volume Event Floods

A batch job that processes 50,000 records and publishes one event per record will hit limits before it finishes.

The publish side: you can publish a maximum of 150 events per transaction using EventBus.publish(). A batch with 50,000 records needs to chunk its publishes across transactions or use the Pub/Sub API from an external system.

The subscription side: an Apex trigger on a Platform Event is invoked with a batch of events — up to 2,000 per invocation, but subject to the standard Apex governor limits that apply to that invocation. At high volume, the event bus delivers events at a rate that respects those limits, which means there's a queue forming behind your trigger.

The org-level limit that actually matters: Event Delivery in the Org Limits UI (Setup > System Overview > Event Monitoring). The default hourly delivery quota is 250,000 event notifications per hour for most editions. When you hit this, delivery queues silently. You will not get an explicit error — events are just delayed.

The failure mode to design for: a flood of 50,000 events at 9 AM does not cause errors. It causes a processing backlog that lasts hours and makes real-time consumers look broken. Size your event volume expectations against this hourly quota.

Fan-Out: One Publisher, Multiple Consumers

Platform Events are a natural fit for fan-out — one published event, multiple independent consumers. Where architects get into trouble is when those consumers have different SLAs.

A practical example: an Order_Placed__e event consumed by three subscribers — fraud detection (must complete in < 500ms), inventory reservation (must complete in < 2s), and an ERP sync that can take up to 30 minutes.

Mixing these in a single Apex trigger creates a coupling problem. If the ERP sync times out, it consumes the trigger's governor limits and potentially delays fraud detection.

The right design separates consumers by SLA tier:

  • Synchronous/near-real-time: Apex trigger with strict governor budget
  • Async with medium SLA: separate Apex trigger, or a second Platform Event channel that the first trigger re-publishes to
  • Long-running/external: the Apex trigger calls a Queueable or a REST endpoint, or publishes to a second channel consumed by an external integration layer

Keep the trigger chain shallow — re-publishing from one Platform Event to another adds latency and doubles your event quota consumption.

Debugging Platform Events in Production

The available tools:

  • Event Bus Stats (Setup > Platform Events > select event > Stats): shows delivered/failed counts and queue depth. Useful for detecting backpressure.
  • Event Monitoring (requires add-on license): the EventDelivery log captures subscriber errors, delivery timestamps, and ReplayIds. Without this license, your visibility into failed deliveries is minimal.
  • Debug Logs: if your subscriber is an Apex trigger, standard debug logging applies. The challenge is correlating a specific event to a specific debug log at high volume.

The critical gap: there is no event inspector in Setup. You cannot browse the event bus contents. You cannot replay events from Setup. The only way to inspect event payloads after the fact is Event Monitoring logs — and those only exist if you have the license and the logging was already enabled.

The practical workaround: log every received event's ReplayId and Correlation_Id__c in your custom log object immediately on receipt, before any processing. This creates your own audit trail that doesn't depend on Event Monitoring.

When Not to Use Platform Events

Platform Events are the right tool when you need asynchronous, decoupled, durable event delivery within or across Salesforce systems. They are the wrong tool when:

  • You need strict ordering on record state changes. Use Change Data Capture — it's ordered per record and requires zero publisher code.
  • You need simple point-to-point integration with an external HTTP endpoint. Outbound Messaging is reliable, ordered, at-least-once, and requires no code. Platform Events for this use case adds complexity with no benefit.
  • You need real-time UI push notifications. The Streaming API (Generic Streaming or PushTopics) has less operational overhead for this use case. Platform Events can do it, but they're heavier.
  • The consumer needs to call out to an external system synchronously. An Apex trigger on a Platform Event cannot make callouts. You'd need to bridge through a Queueable — at which point you should ask whether the original pattern is the right one.

Questions or a different experience? Find me on LinkedIn.

Gabriel Cruz Ferreira

Gabriel Cruz Ferreira

Salesforce Architect · 15x Certified · Road to CTA

Was this post helpful?