Salesforce Platform Events, Change Data Capture, and Pub/Sub API
What are Salesforce Platform Events, Change Data Capture, and Pub/Sub API capabilities and limits?
TL;DR
Bottom line: Salesforce provides three complementary event-driven mechanisms — Platform Events (custom event messages), Change Data Capture (automatic record-change events), and Pub/Sub API (gRPC-based subscribe/publish) — each with distinct limits and use cases. Pub/Sub API is the recommended modern interface for external subscribers. [src4]
Watch out for: CDC has a 5-entity selection limit without add-on license, and events are retained for only 72 hours — if your subscriber goes offline beyond that window, you lose events permanently. [src3, src6]
Best for: Event-driven architectures where external systems need near-real-time notification of Salesforce data changes without polling REST API.
Authentication: OAuth 2.0 (JWT bearer for server-to-server) — same auth as REST API; Pub/Sub API connects via gRPC with OAuth access token in metadata. [src5]
System Profile
Salesforce's event-driven architecture centers on the Event Bus, a unified message backbone that carries Platform Events, Change Data Capture events, and standard platform events. External consumers can subscribe via the legacy CometD-based Streaming API or the newer gRPC-based Pub/Sub API (GA since Spring '22). All new custom platform events default to high-volume since Spring '23; legacy standard-volume events can no longer be created. [src1, src4]
Property
Value
Vendor
Salesforce
System
Salesforce Platform (API v62.0, Spring '26)
API Surface
Platform Events, Change Data Capture, Pub/Sub API, Streaming API
Same retention window as high-volume platform events [src3]
Event retention (legacy standard-volume PE)
24 hours
Rolling
Standard-volume events can no longer be created [src1]
Delivery Counting Methodology
Delivery allocations are consumed per subscriber, not per published event. Publishing 1,000 events to 10 subscribers consumes 10,000 deliveries (1,000 × 10) from the daily allocation. This is the single most impactful limit for fan-out architectures. [src1, src7]
Pub/Sub API requires the OAuth access token in the gRPC call metadata (not as a URL parameter). The token goes in the accesstoken metadata key, and the instance URL in the instanceurl key. [src5]
JWT bearer flow connected apps require a digital certificate — self-signed works for development, but CA-signed is recommended for production. [src5]
Session timeout is configurable by the Salesforce admin. Do not hardcode a 2-hour expiry — handle UNAUTHENTICATED gRPC errors with token refresh. [src5]
Constraints
CDC 5-entity limit: Without the Platform Events add-on license, CDC is limited to 5 selected standard/custom objects per org. This is a hard licensing constraint. [src3]
72-hour replay window: Events older than 72 hours cannot be replayed. If a subscriber is offline for more than 3 days, those events are permanently lost. Design for reconciliation. [src6]
Delivery allocation is per-subscriber: Fan-out patterns multiply delivery consumption. 1 event to N subscribers = N deliveries counted. [src1, src7]
Platform Events are immutable: Published events cannot be updated or deleted. Design event schemas carefully. [src1]
Pub/Sub API payload is Avro binary: Unlike Streaming API (JSON), Pub/Sub API delivers events as Apache Avro binary payloads. Subscribers must fetch the schema and deserialize. [src4, src5]
No guaranteed ordering across partitions: High-volume platform events may be delivered out of order across different transactions. Within a single Apex transaction, events are ordered. [src1]
Integration Pattern Decision Tree
START -- Need event-driven integration with Salesforce
|-- What triggers the event?
| |-- Custom business logic (order placed, status changed)
| | |-- Need custom event schema?
| | | |-- YES --> Platform Events (define __e custom event)
| | | |-- NO --> Standard platform events (LoginEvent, etc.)
| |-- Any record change on specific objects (create/update/delete/undelete)
| | |-- YES --> Change Data Capture (CDC)
| | | |-- Tracking <= 5 objects?
| | | | |-- YES --> CDC works without add-on license
| | | | |-- NO --> Requires Platform Events add-on license
| | |-- Need field-level change tracking?
| | |-- YES --> CDC (delivers changed fields + ChangeEventHeader)
| | |-- NO --> Platform Events (custom schema, publish explicitly)
|-- Where is the subscriber?
| |-- External system (outside Salesforce)
| | |-- Greenfield / new integration?
| | | |-- YES --> Pub/Sub API (gRPC) -- modern, efficient, recommended
| | | |-- NO (existing CometD) --> Streaming API still works, migrate when ready
| | |-- Need > 2,000 concurrent subscribers?
| | |-- YES --> Fan-out via middleware (Heroku, Kafka, EventBridge)
| | |-- NO --> Direct Pub/Sub API subscription
| |-- Inside Salesforce (Apex, Flow, LWC)
| |-- Apex trigger on platform event (after insert)
| |-- Flow: Platform Event-Triggered
| |-- LWC: empApi for real-time UI updates
|-- Volume and reliability?
|-- < 250K events/hour, < 50K deliveries/day?
| |-- YES --> Standard allocation sufficient
| |-- NO --> Purchase Platform Events add-on
|-- Need guaranteed delivery?
|-- YES --> Store replay ID, resubscribe on disconnect
|-- NO --> Fire-and-forget acceptable
1. Define a Platform Event (if using custom events)
Navigate to Setup > Platform Events > New Platform Event. Define fields for your event payload. The API name ends with __e. For CDC, skip this step. [src1]
Setup > Integrations > Platform Events > New Platform Event
- Label: Order Placed
- API Name: Order_Placed__e
- Publish Behavior: Publish After Commit
- Add custom fields: Order_Id__c, Amount__c, Status__c
Verify: GET /services/data/v62.0/sobjects/Order_Placed__e/describe — returns field definitions.
2. Enable CDC for target objects (if using CDC)
Navigate to Setup > Integrations > Change Data Capture. Select up to 5 objects (without add-on). [src3]
Setup > Integrations > Change Data Capture
- Select objects: Account, Opportunity, Contact, Lead, Case
- Save -- CDC begins immediately
- Channel name: /data/AccountChangeEvent, /data/OpportunityChangeEvent, etc.
Verify: Update a record on a selected object — the change event should appear on the corresponding channel within seconds.
3. Connect via Pub/Sub API (gRPC)
Establish a gRPC connection to api.pubsub.salesforce.com:7443. Use OAuth access token in gRPC metadata. [src5]
DateTime fields are ISO 8601 UTC strings in CometD JSON but Avro logical types (long milliseconds since epoch) in Pub/Sub API. Convert carefully. [src3, src4]
Multi-select picklist values are semicolon-delimited strings in CDC events, matching Salesforce API convention. External systems may expect arrays. [src3]
Currency fields in CDC include the value but not the currency code — in multi-currency orgs, CurrencyIsoCode must be tracked separately. [src3]
Use EARLIEST preset or LATEST; full sync may be needed [src6]
Failure Points in Production
Subscriber offline > 72 hours: Events permanently lost. Fix: Implement heartbeat + full reconciliation fallback when gap exceeds 72 hours. [src6]
Fan-out exhausts daily delivery allocation: 50K events to 10 subscribers = 500K deliveries. Fix: Use middleware fan-out (Kafka, EventBridge) with single Salesforce subscriber. [src1, src7]
CDC GAP events during high-volume DML: Salesforce emits GAP events instead of individual changes. Fix: Handle all GAP_ change types; on GAP_OVERFLOW, trigger full sync. [src3]
gRPC channel exhaustion: Hitting 1,000 concurrent streams. Fix: Create multiple gRPC channels (one per event type), each with own connection pool. [src5]
Avro schema mismatch: Schema changes cause deserialization failures. Fix: Always call GetSchema before processing; cache by schema_id; refresh on error. [src5]
Publish callbacks silently failing: EventBus.PublishCallback swallows exceptions. Fix: Log callback results; use Database.SaveResult for individual event failures. [src1]
Anti-Patterns
Wrong: Polling REST API for changes instead of using CDC
# BAD -- Queries all records every 5 minutes to find changes
# Wastes API calls, misses changes between polls
while True:
results = sf.query("SELECT Id, Name FROM Opportunity WHERE SystemModstamp > {last_poll}")
process_changes(results)
time.sleep(300)
Correct: Subscribe to CDC for real-time change notifications
# GOOD -- CDC delivers changes in real-time, zero API call overhead
def handle_cdc_event(event):
header = event["ChangeEventHeader"]
if header["changeType"] in ("CREATE", "UPDATE"):
sync_to_target(header["recordIds"], event)
elif header["changeType"] == "DELETE":
delete_from_target(header["recordIds"])
elif header["changeType"].startswith("GAP_"):
trigger_full_reconciliation(header["entityName"])
Wrong: One CometD subscription per downstream consumer
// BAD -- Each CometD client counts against 2,000 org limit
services.forEach((svc) => {
const client = new CometDClient(sfCredentials);
client.subscribe("/event/Order_Placed__e", svc.handler);
});
Correct: Single subscriber with middleware fan-out
// GOOD -- One subscriber, fan-out via Kafka
const pubsubClient = new SalesforcePubSubClient(credentials);
pubsubClient.subscribe("/event/Order_Placed__e", async (event) => {
await kafkaProducer.send({
topic: "salesforce.order-placed",
messages: [{ value: JSON.stringify(event) }],
});
});
Wrong: Ignoring replay IDs on reconnect
# BAD -- Always subscribes from LATEST, losing events during downtime
subscriber.subscribe(topic="/data/AccountChangeEvent", replay_preset="LATEST")
Correct: Persisting replay IDs for gap-free reconnection
# GOOD -- Stores replay ID, resumes from last position
last_id = load_replay_id(topic)
if last_id:
subscriber.subscribe(topic=topic, replay_id=last_id)
else:
subscriber.subscribe(topic=topic, replay_preset="EARLIEST")
for event in subscriber.events():
process(event)
save_replay_id(topic, event.replay_id)
Common Pitfalls
Exceeding delivery allocation without realizing it: Each subscriber counts separately. 10 triggers + 5 Flows + 3 external = 18 deliveries per event. Fix: Monitor via /services/data/v62.0/limits; consolidate subscribers. [src1, src7]
CDC 5-entity limit surprise: Attempting to select a 6th object without add-on may silently fail. Fix: Check selections via Setup > Change Data Capture before assuming CDC is active. [src3]
Publish After Commit vs Publish Immediately: "Publish After Commit" only publishes if transaction succeeds; "Publish Immediately" publishes even on rollback. Fix: Use Publish After Commit for data consistency. [src1]
Replay ID arithmetic: IDs are not sequential or contiguous. Fix: Store exact replay ID; never compute derived values. [src6]
Avro schema evolution: New fields break old cached schemas. Fix: Fetch schema via GetSchema RPC using schema_id; cache with refresh-on-error. [src5]
Sandbox event limits differ: Sandbox may have lower allocations. Fix: Test with production-like volumes in full sandbox; verify limits via Limits API. [src1]
Need real-time notification of Salesforce record changes
Need to bulk-export historical data
Bulk API 2.0
External system needs to react to Salesforce events
Need to query current record state
REST API SOQL query
Building event-driven microservice architecture
Need request-response pattern
REST API or Composite API
Tracking field-level changes on specific objects (CDC)
Tracking > 5 objects without add-on
Platform Events with Apex triggers
Need guaranteed delivery with replay (within 72h)
Need event retention > 72 hours
External broker (Kafka, AWS SQS)
Fan-out to single middleware subscriber
Fan-out to > 2,000 direct subscribers
Middleware hub-and-spoke pattern
Important Caveats
Platform event allocation limits vary by Salesforce edition and are subject to change each release. Always verify via the Limits REST API (/services/data/v62.0/limits). [src1]
The Platform Events add-on license changes both CDC entity limit (removes 5-object cap) and delivery allocation (+100K/day, shifts to monthly enforcement at 3M/month). Contact Salesforce for pricing. [src1, src3]
Sandbox orgs have independent event allocations that may be lower than production. Performance testing may not reflect production capacity. [src1]
Pub/Sub API is only for external connections — Apex code within Salesforce cannot use gRPC. Use Apex triggers on platform events or empApi in LWC for internal consumption. [src5]
Replay IDs are org-specific and environment-specific. A replay ID from sandbox cannot be used in production. [src6]