Pub/Sub API vs Streaming API (CometD/EMP): Ordering & At-Least-Once Delivery in Salesforce
Salesforce provides two powerful ways to move events in and out of the Event Bus — the modern Pub/Sub API and the Streaming API family (CometD/EMP).
Both deliver events with at-least-once semantics and support replay, but they differ in transport protocols, performance, payload formats, and operational fit. Understanding these differences helps you design resilient, high-throughput integrations that maintain event order and handle duplicates safely.
⚙️ Overview
| Need / Trait | Pub/Sub API | Streaming API (CometD/EMP) |
|---|---|---|
| Transport | gRPC bidirectional streaming (or REST) | CometD (Bayeux) over HTTP long-poll |
| Payload | Protobuf (binary), schema registry; highly efficient | JSON; human-readable and easy to inspect |
| Throughput & Flow Control | High throughput with back-pressure and acknowledgements | Suitable for light/medium traffic; fewer tuning options |
| Publish Support | ✅ Yes (produce & consume) | ❌ Subscribe only (PE/CDC/PB/Generic) |
| Replay Modes | EARLIEST / LATEST / CUSTOM (ReplayId) | Same via Replay Extension |
| Libraries | gRPC clients in multiple languages | EMP Connector (Java), CometD JS, Bayeux clients |
| Operational Fit | Ideal for microservices, pipelines, strict SLAs | Ideal for admin-friendly, low-maintenance subscribers |
? When to Use Which
-
Use Pub/Sub API when you need publish + subscribe, binary efficiency, flow control, or high-volume integrations with strict SLAs.
-
Use Streaming API (CometD/EMP) when you want quick JSON-based subscribers, CDC listeners, or lightweight browser/Java consumers.
? Rule of Thumb:
“If you need typed payloads, back-pressure, and throughput — use Pub/Sub API.
If you need simple, JSON-friendly listeners — use CometD/EMP.”
? Ordering & At-Least-Once Delivery (What You Really Get)
Both APIs deliver messages at least once, not exactly once.
That means duplicates can happen — due to retries, reconnects, or replay.
To keep your processing safe:
-
Always design idempotent consumers.
-
Persist replay checkpoints.
-
Partition ordering by logical business keys.
Delivery Semantics
-
At-Least-Once: Duplicates are possible. Your consumer must handle them safely.
-
Ordering: Salesforce does not guarantee global ordering.
-
Within a transaction, CDC events include
ChangeEventHeaderwithtransactionKeyandsequenceNumber— you can locally reorder using these. -
Across transactions, order may vary. To avoid cross-key chaos, partition by a key like
AccountIdorOrderId.
-
Replay
Both APIs support:
-
LATEST -
EARLIEST -
CUSTOM(from stored ReplayId)
Best practice: Store checkpoints after each successful processing cycle.
? Idempotency & Deduplication
Since events may be re-delivered, you must make your consumers idempotent — safe to reprocess the same message multiple times.
Key pattern: Claim-Then-Act
-
Build a unique deduplication key, e.g.:
${RecordId}:${transactionKey}:${sequenceNumber}for CDC, orDedupKeyfor Platform Events. -
Insert this key into a unique storage record (e.g.,
Idempotency__c.Key__c). -
If the insert succeeds → process the event.
-
If it fails (duplicate key) → skip the side effects.
Keep deduplication entries for at least the event retention period to prevent reprocessing old events.
? Real-World Example (Section-Wise)
Scenario
You operate a billing microservice that:
-
Subscribes to Platform Events (
InvoiceIssued__e) using the Pub/Sub API for high volume. -
Consumes Account CDC via CometD/EMP for cache invalidation.
-
Enforces idempotency and CUSTOM replay for reliability.
A) Pub/Sub API (Node.js-style, gRPC) — Subscribe with Back-Pressure & CUSTOM Replay
// Pseudo-code: subscribe to /event/InvoiceIssued__e with a stored checkpoint
const topic = '/event/InvoiceIssued__e';
let lastReplayId = await checkpointRepo.load(topic); // Buffer or base64
const request = {
topicName: topic,
replayPreset: lastReplayId ? 'CUSTOM' : 'LATEST',
replayId: lastReplayId || null,
numRequested: 500 // flow control: server will stream up to this many
};
const stream = await pubsubClient.subscribe(request);
for await (const message of stream) {
const replayId = message.replayId; // binary
const payload = decodeProtobuf(message.payload); // your POJO
// Idempotency: claim before side effects
const key = payload.dedupKey || `${payload.invoiceId}:${payload.version}`;
const claimed = await idempotencyRepo.claim(key);
if (!claimed) { await checkpointRepo.save(topic, replayId); continue; }
await billing.applyInvoice(payload); // DB upsert, external calls, etc.
await checkpointRepo.save(topic, replayId); // commit checkpoint last
// Stream continues; back-pressure by awaiting work before reading next
}
Why Pub/Sub API here?
-
Binary payloads are compact and efficient.
-
Flow control prevents overload.
-
Supports both producing and consuming events.
B) Streaming API via EMP Connector (Java) — CDC with JSON & Replay
// Pseudo-code using EMP Connector to consume AccountChangeEvent
String channel = "/data/AccountChangeEvent";
Long replayFrom = checkpointRepo.load(channel).orElse(EmpConnector.REPLAY_FROM_TIP);
BearerTokenProvider tokenProvider = new BearerTokenProvider(() -> yourOAuthToken());
EmpConnector emp = new EmpConnector(new BayeuxParameters(tokenProvider));
emp.start().get(30, TimeUnit.SECONDS);
Subscription subscription = emp.subscribe(channel, replayFrom, event -> {
Map<String, Object> header = (Map<String, Object>) event.get("ChangeEventHeader");
String tx = (String) header.get("transactionKey");
Integer seq = ((Number) header.get("sequenceNumber")).intValue();
String recordId = ((List<String>) header.get("recordIds")).get(0);
String dedup = recordId + ":" + tx + ":" + seq;
if (!idempotencyRepo.claim(dedup)) return; // already seen
// Process: read latest Account state if needed
accountCache.invalidate(recordId);
// Update checkpoint after success
checkpointRepo.save(channel, (Long) event.get("replayId"));
}).get(30, TimeUnit.SECONDS);
Why EMP here?
-
JSON payloads are lightweight and easy to handle.
-
Perfect for CDC and quick admin-managed integrations.
-
EMP Connector (Java) makes implementation simple.
C) Apex Publisher (Optional) — Produce Platform Events for Pub/Sub Consumers
public with sharing class InvoicePublisher {
public static void publishIssued(Id invoiceId, Decimal amount, String currency, String dedupKey) {
InvoiceIssued__e e = new InvoiceIssued__e(
InvoiceId__c = (String)invoiceId,
Amount__c = amount,
Currency__c = currency,
DedupKey__c = dedupKey // e.g., invoiceId:issued:v1
);
EventBus.publish(e);
}
}

? Final Thoughts
Both Pub/Sub API and Streaming API deliver at-least-once with replay capabilities — so your design must ensure idempotency and checkpoint persistence.
-
Choose Pub/Sub API for:
-
High-throughput pipelines
-
Binary payloads (Protobuf)
-
Fine-grained flow control
-
Event publishing and consumption
-
-
Choose CometD/EMP for:
-
JSON-based, human-readable messages
-
Quick CDC subscribers or lightweight UIs
-
Low operational complexity
-
And remember:
Ordering is a per-key concept — use CDC’s transaction metadata or your own correlation IDs, and never depend on cross-key ordering.
This approach keeps your event-driven architecture resilient, idempotent, and production-grade.
