---
# === IDENTITY ===
id: business/erp-integration/salesforce-streaming-platform-events/2026
canonical_question: "What are Salesforce Platform Events, Change Data Capture, and Pub/Sub API capabilities and limits?"
aliases:
  - "Salesforce Platform Events limits and allocations"
  - "Salesforce Change Data Capture CDC configuration and limits"
  - "Salesforce Pub/Sub API gRPC vs Streaming API CometD"
  - "Salesforce event-driven architecture streaming options"
entity_type: erp_integration
domain: business > erp-integration > salesforce-streaming-platform-events
region: global
jurisdiction: global
temporal_scope: 2025-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Salesforce Platform"
    vendor: "Salesforce"
    version: "API v62.0 (Spring '26)"
    edition: "Enterprise, Performance, Unlimited, Developer"
    deployment: cloud
    api_surface: "Platform Events, Change Data Capture, Pub/Sub API (gRPC), Streaming API (CometD)"

# === VERIFICATION ===
last_verified: 2026-03-01
confidence: 0.90
version: 1.0
first_published: 2026-03-01

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "Spring '23 — all new platform events default to high-volume; standard-volume event creation disabled"
  next_review: 2026-08-28
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "High-volume platform events: 250,000 publishes/hour (Performance/Unlimited); delivery allocation counted per subscriber (1,000 events x 10 subscribers = 10,000 deliveries)"
  - "CDC entity selection limit: 5 standard/custom objects without add-on license"
  - "Pub/Sub API: max 100 events per FetchRequest, 1,000 concurrent gRPC streams per channel, 1 MB per event message, 4 MB hard gRPC limit per PublishRequest"
  - "Event retention: 72 hours for high-volume platform events and CDC; 24 hours for legacy standard-volume events"
  - "Concurrent CometD subscribers: 2,000 per org (Streaming API)"
  - "Managed subscriptions: max 200 unique managed subscriptions per org (Pub/Sub API)"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs Salesforce REST API for CRUD operations, not event-driven patterns"
    use_instead: "business/erp-integration/salesforce-rest-api/2026"
  - condition: "User needs Salesforce Bulk API for batch/ETL data loads"
    use_instead: "business/erp-integration/salesforce-bulk-api/2026"
  - condition: "User needs event-driven integration with SAP or Oracle, not Salesforce"
    use_instead: "business/erp-integration/sap-event-mesh/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "file-based (CSV/XML import/export)"
  - key: data_volume
    question: "What's your daily data volume?"
    type: choice
    options:
      - "< 1,000 records/day"
      - "1,000-100,000 records/day"
      - "> 100,000 records/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to this ERP)"
      - "outbound (reading from this ERP)"
      - "bidirectional sync"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/salesforce-streaming-platform-events/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-01)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/salesforce-rest-api/2026"
      label: "Salesforce REST API capabilities and rate limits"
    - id: "business/erp-integration/salesforce-bulk-api/2026"
      label: "Salesforce Bulk API 2.0 for high-volume data operations"
  solves: []
  alternative_to:
    - id: "business/erp-integration/salesforce-outbound-messages/2026"
      label: "Salesforce Outbound Messages (legacy webhook alternative)"
  often_confused_with:
    - id: "business/erp-integration/salesforce-streaming-api-legacy/2026"
      label: "Legacy Streaming API (CometD/Bayeux) — predecessor to Pub/Sub API"

# === SOURCES ===
sources:
  - id: src1
    title: "Platform Event Allocations — Platform Events Developer Guide"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_event_limits.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src2
    title: "Pub/Sub API Allocations"
    author: Salesforce
    url: https://developer.salesforce.com/docs/platform/pub-sub-api/guide/allocations.html
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src3
    title: "Change Data Capture Allocations — CDC Developer Guide"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.change_data_capture.meta/change_data_capture/cdc_allocations.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src4
    title: "Streaming API vs. Pub/Sub API — Streaming API Developer Guide"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.api_streaming.meta/api_streaming/pubsub_api_streaming_api_comparison.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src5
    title: "Pub/Sub API as a gRPC API"
    author: Salesforce
    url: https://developer.salesforce.com/docs/platform/pub-sub-api/guide/grpc-api.html
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src6
    title: "Event Message Durability — Pub/Sub API"
    author: Salesforce
    url: https://developer.salesforce.com/docs/platform/pub-sub-api/guide/event-message-durability.html
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src7
    title: "How to Work Within Platform Events Delivery Limits"
    author: Salesforce
    url: https://developer.salesforce.com/blogs/2021/08/how-to-work-within-platform-events-delivery-limits
    type: technical_blog
    published: 2021-08-01
    reliability: high
  - id: src8
    title: "Pub/Sub API: Building Event-Driven Integrations Just Got Even Easier"
    author: Salesforce
    url: https://developer.salesforce.com/blogs/2021/07/pub-sub-api-building-event-driven-integrations-just-got-even-easier
    type: technical_blog
    published: 2021-07-01
    reliability: high
---

# Salesforce Platform Events, Change Data Capture, and Pub/Sub API

## 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; CometD-based Streaming API still works but is less efficient. [src4]
- **Key limit**: 250,000 platform event publishes/hour (Performance/Unlimited edition); delivery allocation is per-subscriber, so 1,000 events to 10 subscribers = 10,000 deliveries against daily quota. [src1]
- **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. This card covers all three event mechanisms and both consumption APIs. [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 |
| **Current API Version** | v62.0 |
| **Editions Covered** | Enterprise, Performance, Unlimited, Developer |
| **Deployment** | Cloud |
| **API Docs URL** | [Platform Events Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro.htm) |
| **Status** | GA (Pub/Sub API GA since Spring '22; Streaming API GA, not deprecated) |

## API Surfaces & Capabilities

| API Surface | Protocol | Best For | Event Retention | Rate Limit | Real-time? | Direction |
|---|---|---|---|---|---|---|
| Platform Events (High-Volume) | Event Bus | Custom event messages between systems | 72 hours | 250K publishes/hour (Perf/Unlim) | Yes | Publish + Subscribe |
| Change Data Capture (CDC) | Event Bus | Automatic record-change notifications | 72 hours (3 days) | Shared with platform event delivery allocation | Yes | Subscribe only |
| Pub/Sub API | gRPC / HTTP/2 / Avro | External subscription, high throughput | N/A (consumes from Event Bus) | 1,000 concurrent gRPC streams/channel | Yes | Publish + Subscribe |
| Streaming API | CometD / Bayeux / HTTP/1.1 / JSON | Legacy external subscription | N/A (consumes from Event Bus) | 2,000 concurrent CometD clients/org | Yes | Subscribe only |

[src1, src2, src3, src4]

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max events per FetchRequest | 100 | Pub/Sub API Subscribe RPC | Maximum num_requested value per request [src2] |
| Max event message size | 1 MB | Pub/Sub API PublishRequest | Per individual event in a batch [src2] |
| Recommended batch size | 200 events | Pub/Sub API PublishRequest | For best performance; total batch < 3 MB recommended [src2] |
| Hard gRPC message limit | 4 MB | Pub/Sub API PublishRequest | Exceeding causes publish failure and stream closure [src5] |
| Concurrent gRPC streams | 1,000 | Pub/Sub API per channel | Active RPC calls on same gRPC channel [src5] |
| Max managed subscriptions | 200 | Pub/Sub API per org | Unique managed subscriptions per Salesforce org [src2] |
| Max custom fields per PE | 25 | Platform Event definition | Per platform event definition [src1] |

### Rolling / Daily Limits

| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| Platform Event hourly publishing | 250,000 | 1 hour | Performance/Unlimited: 250K; add-on adds +25K/hour [src1] |
| Platform Event + CDC daily delivery | 50,000 base | 24 hours | Performance/Unlimited: 50K; add-on adds +100K/day (shifts to 3M/month) [src1] |
| CDC entity selection | 5 objects | Ongoing | All editions: 5 entities without add-on; add-on removes limit [src3] |
| Concurrent CometD clients | 2,000 | Per org | Shared across all Streaming API subscriptions [src7] |
| Event retention (high-volume PE) | 72 hours | Rolling | Events beyond 72h are permanently deleted [src6] |
| Event retention (CDC) | 72 hours (3 days) | Rolling | 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 x 10) from the daily allocation. This is the single most impactful limit for fan-out architectures. [src1, src7]

## Authentication

| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 JWT Bearer | Server-to-server Pub/Sub API connections | Session timeout (default 2h) | New JWT per request | Recommended for unattended integrations [src5] |
| OAuth 2.0 Web Server | User-context event publishing | Access: 2h, Refresh: until revoked | Yes | Requires callback URL |
| Connected App + Client Credentials | Service-to-service without user context | Configurable | Yes | Available since Winter '23 |

### Authentication Gotchas

- Pub/Sub API requires the OAuth access token in the gRPC call metadata (not as a URL parameter or header like REST API). 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. The certificate must be uploaded to the connected app before first use. [src5]
- Session timeout is configurable by the Salesforce admin (Setup > Session Settings). Do not hardcode a 2-hour expiry — check the `Sforce-Limit-Info` header or handle `UNAUTHENTICATED` gRPC errors with token refresh. [src5]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- **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, not a technical one. [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 (full sync fallback). [src6]
- **Delivery allocation is per-subscriber**: Fan-out patterns multiply delivery consumption. 1 event to N subscribers = N deliveries counted. High fan-out can exhaust daily allocation quickly. [src1, src7]
- **Platform Events are immutable**: Published events cannot be updated or deleted. Design event schemas carefully — you cannot modify an event after publication. [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 event schema (GetSchema RPC) and deserialize. This is more efficient but adds client complexity. [src4, src5]
- **No guaranteed ordering across partitions**: High-volume platform events may be delivered out of order when published 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 to define custom event schema?
|   |   |   |-- YES --> Platform Events (define __e custom event)
|   |   |   |-- NO --> Use standard platform events (LoginEvent, LogoutEvent, 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 client) --> Streaming API still works, migrate when ready
|   |   |-- Need > 2,000 concurrent subscribers?
|   |       |-- YES --> Fan-out via middleware (Heroku, Kafka, AWS EventBridge)
|   |       |-- NO --> Direct Pub/Sub API subscription
|   |-- Inside Salesforce (Apex, Flow, LWC)
|       |-- Apex trigger on platform event (after insert)
|       |-- Flow: Record-Triggered or Platform Event-Triggered
|       |-- LWC: empApi (lightning/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 (+25K/hr, +100K/day)
    |-- Need guaranteed delivery?
        |-- YES --> Store replay ID, resubscribe with replay on disconnect
        |-- NO --> Fire-and-forget acceptable
```

## Quick Reference

### Platform Events vs CDC vs Pub/Sub API

| Capability | Platform Events | Change Data Capture | Pub/Sub API |
|---|---|---|---|
| **What it does** | Custom event messages you define and publish | Automatic events on record CUD operations | gRPC interface to subscribe/publish events |
| **Event schema** | Custom (__e object definition) | Automatic (mirrors object fields) | Consumes PE + CDC events |
| **Publishing** | Apex, API, Flow, Process Builder | Automatic (system-generated) | gRPC PublishRequest |
| **Subscribing** | Apex trigger, Flow, empApi, CometD, gRPC | Apex trigger, Flow, empApi, CometD, gRPC | gRPC Subscribe/ManagedSubscribe RPC |
| **Retention** | 72h (high-volume) | 72h (3 days) | N/A (reads from Event Bus) |
| **Replay support** | Yes (replay ID) | Yes (replay ID) | Yes (replay ID or custom replay preset) |
| **Payload format (external)** | JSON (CometD) / Avro (Pub/Sub) | JSON (CometD) / Avro (Pub/Sub) | Avro binary only |
| **Protocol** | Via Streaming API or Pub/Sub API | Via Streaming API or Pub/Sub API | gRPC / HTTP/2 |
| **Ordering** | Ordered within single Apex transaction | Ordered per object per transaction | Preserved from source |
| **Filtering** | Subscriber-side only | Subscriber-side only | Subscriber-side only |

[src1, src3, src4]

### Streaming API (CometD) vs Pub/Sub API (gRPC)

| Feature | Streaming API (CometD) | Pub/Sub API (gRPC) |
|---|---|---|
| **Protocol** | HTTP/1.1, Bayeux/CometD | HTTP/2, gRPC |
| **Payload format** | JSON | Apache Avro (binary, compressed) |
| **Delivery model** | Push (server pushes to client) | Pull (client requests N events via FetchRequest) |
| **Flow control** | None (server controls pace) | Client-controlled (set num_requested) |
| **Efficiency** | Higher bandwidth, larger payloads | ~7-10x faster than REST/JSON; compressed byte buffers |
| **Language support** | JavaScript (CometD client) | 11+ languages (Python, Java, Node, C++, Go, etc.) |
| **Publishing** | Not supported (subscribe only) | Supported (PublishRequest RPC) |
| **Concurrent limit** | 2,000 CometD clients/org | 1,000 streams/gRPC channel (no org-wide cap) |
| **Managed subscriptions** | No | Yes (ManagedSubscribe, up to 200/org) |
| **Deprecation status** | Not deprecated, but no new features | Actively developed, recommended for new projects |

[src4, src5]

## Step-by-Step Integration Guide

### 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` (e.g., `Order_Placed__e`). For CDC, skip this step — CDC events are auto-generated from object changes. [src1]

```
Setup > Integrations > Platform Events > New Platform Event
- Label: Order Placed
- API Name: Order_Placed__e
- Publish Behavior: Publish After Commit (recommended for transactional consistency)
- Add custom fields: Order_Id__c, Amount__c, Status__c
```

**Verify**: Query the event definition via REST: `GET /services/data/v62.0/sobjects/Order_Placed__e/describe` — should return 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). CDC events are published automatically on create, update, delete, and undelete operations on selected objects. [src3]

```
Setup > Integrations > Change Data Capture
- Select objects: Account, Opportunity, Contact, Lead, Case
- Save — CDC begins immediately, no deployment needed
- 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 the Pub/Sub API endpoint. Use OAuth access token in gRPC metadata. The endpoint is `api.pubsub.salesforce.com:7443`. [src5]

```python
# Input:  OAuth access token, Salesforce instance URL
# Output: gRPC channel connected to Pub/Sub API

import grpc
from google.protobuf import empty_pb2

# Pub/Sub API endpoint (same for all orgs)
PUBSUB_ENDPOINT = "api.pubsub.salesforce.com:7443"

# Create secure gRPC channel
channel_credentials = grpc.ssl_channel_credentials()
channel = grpc.secure_channel(PUBSUB_ENDPOINT, channel_credentials)

# Metadata for authentication
metadata = (
    ("accesstoken", "YOUR_OAUTH_ACCESS_TOKEN"),
    ("instanceurl", "https://yourinstance.my.salesforce.com"),
    ("tenantid", "YOUR_ORG_ID"),
)
```

**Verify**: Call `GetTopic` RPC with your event channel name — should return topic metadata including schema ID.

### 4. Subscribe to events and handle replay

Use the Subscribe RPC to receive events. Store the replay ID after each batch to resume on reconnect. Request up to 100 events per FetchRequest. [src2, src6]

```python
# Input:  gRPC stub, topic name, optional replay_id
# Output: Stream of event messages with replay IDs

def subscribe_to_events(stub, topic_name, replay_preset=None, replay_id=None):
    """Subscribe to a Salesforce event channel via Pub/Sub API."""
    fetch_request = {
        "topic_name": topic_name,
        "num_requested": 100,  # max allowed per FetchRequest
    }
    if replay_id:
        fetch_request["replay_id"] = replay_id
    elif replay_preset:
        # LATEST = only new events; EARLIEST = from start of retention window
        fetch_request["replay_preset"] = replay_preset

    # Subscribe returns a bidirectional stream
    for response in stub.Subscribe(iter([fetch_request]), metadata=metadata):
        for event in response.events:
            # Decode Avro payload using schema from GetSchema RPC
            payload = decode_avro(event.event.payload, response.schema_id)
            print(f"Event: {payload}")
            # IMPORTANT: Store this replay_id for reconnection
            last_replay_id = event.replay_id
        # Request more events (flow control)
        yield {"topic_name": topic_name, "num_requested": 100}
```

**Verify**: Publish a test event (Apex, Flow, or API) — the subscriber should receive it within seconds. Store `replay_id` and verify reconnection resumes from the correct position.

## Code Examples

### Python: Subscribe to CDC events via Pub/Sub API

```python
# Input:  Salesforce OAuth credentials, object to track (e.g., Account)
# Output: Real-time stream of record changes with changed field values

import grpc
import avro.io
import avro.schema
import io
import json

PUBSUB_ENDPOINT = "api.pubsub.salesforce.com:7443"

class SalesforcePubSubClient:
    def __init__(self, access_token, instance_url, org_id):
        self.metadata = (
            ("accesstoken", access_token),
            ("instanceurl", instance_url),
            ("tenantid", org_id),
        )
        credentials = grpc.ssl_channel_credentials()
        self.channel = grpc.secure_channel(PUBSUB_ENDPOINT, credentials)
        # Import the generated proto stubs
        # from pubsub_api_pb2_grpc import PubSubStub
        # self.stub = PubSubStub(self.channel)

    def get_schema(self, schema_id):
        """Fetch and parse Avro schema for event deserialization."""
        # response = self.stub.GetSchema(
        #     GetSchemaRequest(schema_id=schema_id),
        #     metadata=self.metadata
        # )
        # return avro.schema.parse(response.schema_json)
        pass

    def decode_event(self, payload, schema):
        """Decode Avro binary payload to Python dict."""
        reader = avro.io.DatumReader(schema)
        decoder = avro.io.BinaryDecoder(io.BytesIO(payload))
        return reader.read(decoder)

    def subscribe(self, topic, num_requested=100, replay_preset="LATEST"):
        """Subscribe to event topic with flow control."""
        # Use Subscribe RPC with FetchRequest stream
        # Store replay_id for reconnection after failures
        pass
```

### JavaScript/Node.js: Subscribe to Platform Events via CometD

```javascript
// Input:  Salesforce OAuth token, event channel name
// Output: Real-time event notifications via CometD long-polling

// npm install cometd cometd-nodejs-client jsforce@2
const jsforce = require("jsforce"); // v2.x
const cometd = require("cometd");
const cometdAdapter = require("cometd-nodejs-client");
cometdAdapter.adapt();

const conn = new jsforce.Connection({
  loginUrl: "https://login.salesforce.com",
  accessToken: process.env.SF_ACCESS_TOKEN,
  instanceUrl: process.env.SF_INSTANCE_URL,
});

// Subscribe to a Platform Event channel
const channel = "/event/Order_Placed__e";
const replayId = -1; // -1 = new events only; -2 = all in retention window

conn.streaming.topic(channel).subscribe((message) => {
  console.log("Event received:", JSON.stringify(message, null, 2));
  // message.payload contains event fields
  // message.event.replayId contains the replay ID for reconnection
  const replayId = message.event.replayId;
  // Store replayId persistently for reconnection
});

// Subscribe to CDC channel
const cdcChannel = "/data/AccountChangeEvent";
conn.streaming.topic(cdcChannel).subscribe((message) => {
  const header = message.payload.ChangeEventHeader;
  console.log(`Change type: ${header.changeType}`);
  console.log(`Changed fields: ${header.changedFields}`);
  console.log(`Record IDs: ${header.recordIds}`);
});
```

### cURL: Publish a Platform Event via REST API

```bash
# Input:  OAuth access token, instance URL, platform event API name
# Output: Event published to Event Bus, returns event ID

# Publish a single platform event
curl -X POST \
  "https://yourinstance.my.salesforce.com/services/data/v62.0/sobjects/Order_Placed__e" \
  -H "Authorization: Bearer ${SF_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "Order_Id__c": "ORD-2026-001",
    "Amount__c": 1500.00,
    "Status__c": "Confirmed"
  }'

# Expected response: {"id":"e01xx0000000001AAA","success":true,"errors":[]}

# Check platform event usage limits
curl -X GET \
  "https://yourinstance.my.salesforce.com/services/data/v62.0/limits" \
  -H "Authorization: Bearer ${SF_ACCESS_TOKEN}" \
  | jq '.HourlyPublishedPlatformEvents, .DailyDeliveredPlatformEvents'

# Expected: {"Max":250000,"Remaining":249999}, {"Max":50000,"Remaining":49999}
```

## Data Mapping

### CDC ChangeEventHeader Fields

| Field | Type | Description | Gotcha |
|---|---|---|---|
| `changeType` | String | CREATE, UPDATE, DELETE, UNDELETE, GAP_CREATE, GAP_UPDATE, GAP_DELETE, GAP_UNDELETE, GAP_OVERFLOW | GAP_ types indicate missed events — trigger full reconciliation [src3] |
| `changedFields` | String[] | List of field API names that changed | Empty for CREATE (all fields included in body) [src3] |
| `recordIds` | String[] | IDs of affected records | Can contain multiple IDs when batched in same transaction [src3] |
| `commitTimestamp` | Long | Epoch timestamp of the DML commit | Not the time the event was published — there can be a delay [src3] |
| `transactionKey` | String | Unique ID for the transaction | Use to group events from the same Apex transaction [src3] |
| `sequenceNumber` | Integer | Order within transaction | Events with same transactionKey are ordered by this value [src3] |
| `entityName` | String | SObject API name | Use to route events from compound channels like `/data/ChangeEvents` [src3] |
| `commitUser` | String | User ID who made the change | May be an integration user or automation user [src3] |

### Platform Event Payload vs CDC Payload

| Aspect | Platform Events | Change Data Capture |
|---|---|---|
| Schema definition | Custom (you define fields) | Automatic (mirrors object fields) |
| Payload content | Only fields you define | Changed fields + ChangeEventHeader |
| Null handling | Explicit null for empty fields | Only changed fields included (sparse payload) |
| Record ID | Custom field if needed | In ChangeEventHeader.recordIds |
| Change type | Custom field if needed | Automatic (CREATE/UPDATE/DELETE/UNDELETE) |

### Data Type Gotchas

- CDC event field values use Salesforce API types — 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 delivered as semicolon-delimited strings in CDC events, matching the 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, the CurrencyIsoCode field must be tracked separately. [src3]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| `OPERATION_TOO_LARGE` | Event payload exceeds 1 MB | Single event message too large | Reduce payload size; split into multiple events [src2] |
| `EXCEEDED_ID_LIMIT` | Too many IDs in operation | Batch exceeds limits | Reduce batch size in PublishRequest [src2] |
| `UNAVAILABLE` (gRPC) | Server temporarily unavailable | Service disruption or deployment | Exponential backoff retry: 1s, 2s, 4s, 8s, max 60s [src5] |
| `UNAUTHENTICATED` (gRPC) | Invalid or expired token | OAuth token expired | Refresh token and reconnect with new metadata [src5] |
| `RESOURCE_EXHAUSTED` (gRPC) | Rate limit exceeded | Exceeds concurrent stream or publishing limit | Reduce concurrent streams or publishing rate [src2, src5] |
| `GAP_OVERFLOW` | CDC events lost | Event volume exceeded delivery capacity | Full data reconciliation required — cannot recover lost events [src3] |
| `INVALID_REPLAY_ID` | Replay ID outside retention window | Replay ID older than 72 hours | Subscribe with EARLIEST preset or LATEST; full sync may be needed [src6] |

### Failure Points in Production

- **Subscriber offline > 72 hours**: Events beyond the retention window are permanently lost. Fix: `Implement a heartbeat mechanism that triggers full data reconciliation if the subscriber detects a gap > 72 hours. Store last successful replay ID with timestamp.` [src6]
- **Fan-out exhausts daily delivery allocation**: Publishing 50,000 events to 10 subscribers consumes 500,000 deliveries — well above the 50K base allocation. Fix: `Use middleware fan-out (Heroku, Kafka, EventBridge) with a single Salesforce subscriber that rebroadcasts to downstream consumers.` [src1, src7]
- **CDC GAP events during high-volume DML**: When too many records change simultaneously, Salesforce may emit GAP events instead of individual change events. Fix: `Handle all GAP_ change types in subscriber logic — on GAP_OVERFLOW, trigger a full sync for affected objects.` [src3]
- **gRPC channel exhaustion**: Hitting 1,000 concurrent streams on a single gRPC channel. Fix: `Create multiple gRPC channels (one per event type or function), each with its own connection pool.` [src5]
- **Pub/Sub API Avro schema mismatch**: Schema changes (field additions/removals on platform event) cause deserialization failures. Fix: `Always call GetSchema before processing events; cache schemas by schema_id and refresh on INVALID_ARGUMENT errors.` [src5]
- **Publish callbacks silently failing**: Apex publish callbacks (EventBus.PublishCallback) swallow exceptions by default. Fix: `Always log callback results; use Database.SaveResult to check for individual event failures in batch publishes.` [src1]

## Anti-Patterns

### Wrong: Polling REST API for changes instead of using CDC

```python
# BAD — Queries all records every 5 minutes to find changes
# Wastes API calls, misses changes between polls, doesn't scale
import time
while True:
    results = sf.query(
        "SELECT Id, Name, Amount FROM Opportunity "
        "WHERE SystemModstamp > {last_poll_time}"
    )
    process_changes(results)
    time.sleep(300)  # 5-minute polling interval
```

### Correct: Subscribe to CDC for real-time change notifications

```python
# GOOD — CDC delivers changes in real-time, zero API call overhead
# Only changed fields are delivered (sparse payload)
def handle_cdc_event(event):
    header = event["ChangeEventHeader"]
    if header["changeType"] in ("CREATE", "UPDATE"):
        changed_fields = {
            k: v for k, v in event.items()
            if k != "ChangeEventHeader" and v is not None
        }
        sync_to_target(header["recordIds"], changed_fields)
    elif header["changeType"] == "DELETE":
        delete_from_target(header["recordIds"])
    elif header["changeType"].startswith("GAP_"):
        trigger_full_reconciliation(header["entityName"])
```

### Wrong: Creating one CometD subscription per downstream consumer

```javascript
// BAD — Each CometD client counts against 2,000 org limit
// 100 microservices = 100 CometD clients = 5% of org capacity
services.forEach((service) => {
  const client = new CometDClient(sfCredentials);
  client.subscribe("/event/Order_Placed__e", service.handler);
});
```

### Correct: Single subscriber with middleware fan-out

```javascript
// GOOD — One Pub/Sub API subscriber, fan-out via message broker
// Consumes 1 subscriber slot, supports unlimited downstream consumers
const pubsubClient = new SalesforcePubSubClient(credentials);
const kafkaProducer = new KafkaProducer(kafkaConfig);

pubsubClient.subscribe("/event/Order_Placed__e", async (event) => {
  // Rebroadcast to Kafka topic — downstream services subscribe to Kafka
  await kafkaProducer.send({
    topic: "salesforce.order-placed",
    messages: [{ value: JSON.stringify(event) }],
  });
});
```

### Wrong: Ignoring replay IDs and losing events on reconnect

```python
# BAD — Always subscribes from LATEST, losing all events during downtime
def connect():
    subscriber.subscribe(
        topic="/data/AccountChangeEvent",
        replay_preset="LATEST"  # Misses all events since last disconnect
    )
```

### Correct: Persisting replay IDs for gap-free reconnection

```python
# GOOD — Stores replay ID after each batch, resumes from last position
import json
import os

REPLAY_FILE = "replay_state.json"

def load_replay_id(topic):
    if os.path.exists(REPLAY_FILE):
        state = json.load(open(REPLAY_FILE))
        return state.get(topic)
    return None

def save_replay_id(topic, replay_id):
    state = json.load(open(REPLAY_FILE)) if os.path.exists(REPLAY_FILE) else {}
    state[topic] = replay_id
    json.dump(state, open(REPLAY_FILE, "w"))

def connect(topic):
    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**: Delivery is counted per subscriber. If you have 10 Apex triggers, 5 Flows, and 3 external subscribers all listening to the same event, each event publication costs 18 deliveries. Fix: `Monitor via /services/data/v62.0/limits — check DailyDeliveredPlatformEvents. Consolidate subscribers where possible.` [src1, src7]
- **CDC selected entity limit surprise**: The 5-entity limit is a licensing constraint. Attempting to select a 6th object without add-on license silently fails in some implementations. Fix: `Check current selections via Setup > Change Data Capture before assuming CDC is active on an object. Consider Platform Events as alternative for objects beyond the 5-entity limit.` [src3]
- **Publish After Commit vs Publish Immediately confusion**: "Publish After Commit" (default for high-volume) means events are only published if the Apex transaction commits successfully. "Publish Immediately" publishes even if the transaction rolls back. Fix: `Use Publish After Commit for data consistency. Only use Publish Immediately for fire-and-forget notifications.` [src1]
- **Replay ID is opaque — don't do arithmetic on it**: Replay IDs are not guaranteed to be sequential or contiguous. You cannot compute replay_id + 1 to get the next event. Fix: `Store and replay the exact replay ID value returned by the system. Never compute derived replay IDs.` [src6]
- **Avro deserialization failures after schema evolution**: When platform event fields are added or removed, the Avro schema changes. Subscribers caching the old schema will fail to deserialize new events. Fix: `Always fetch the schema from GetSchema RPC using the schema_id in the response. Implement schema caching with refresh-on-error logic.` [src5]
- **Sandbox has different event limits**: Sandbox orgs may have lower platform event allocations than production. Events published in sandbox count against sandbox-specific limits. Fix: `Test with production-like event volumes in a full sandbox, not a developer sandbox. Check sandbox limits via the Limits API.` [src1]

## Diagnostic Commands

```bash
# Check platform event usage / remaining limits
curl -s "https://${SF_INSTANCE}/services/data/v62.0/limits" \
  -H "Authorization: Bearer ${SF_ACCESS_TOKEN}" \
  | jq '{
    HourlyPublishedPlatformEvents: .HourlyPublishedPlatformEvents,
    DailyDeliveredPlatformEvents: .DailyDeliveredPlatformEvents,
    HourlyPublishedStandardVolumePlatformEvents: .HourlyPublishedStandardVolumePlatformEvents
  }'

# List all platform event definitions in the org
curl -s "https://${SF_INSTANCE}/services/data/v62.0/sobjects" \
  -H "Authorization: Bearer ${SF_ACCESS_TOKEN}" \
  | jq '[.sobjects[] | select(.name | endswith("__e")) | {name, label, custom}]'

# Describe a specific platform event's schema
curl -s "https://${SF_INSTANCE}/services/data/v62.0/sobjects/Order_Placed__e/describe" \
  -H "Authorization: Bearer ${SF_ACCESS_TOKEN}" \
  | jq '{name, fields: [.fields[] | {name, type, length}]}'

# Test publishing a platform event
curl -X POST "https://${SF_INSTANCE}/services/data/v62.0/sobjects/Order_Placed__e" \
  -H "Authorization: Bearer ${SF_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"Order_Id__c":"TEST-001","Amount__c":100,"Status__c":"Test"}'

# Check CDC entity selections (requires Tooling API)
curl -s "https://${SF_INSTANCE}/services/data/v62.0/tooling/query?q=SELECT+Id,DurableId,DeveloperName+FROM+PlatformEventChannelMember" \
  -H "Authorization: Bearer ${SF_ACCESS_TOKEN}"
```

## Version History & Compatibility

| API/Feature | Release | Status | Key Changes | Notes |
|---|---|---|---|---|
| Pub/Sub API | Spring '22 (v54.0) | GA | Initial GA release — gRPC Subscribe + Publish | Replaced need for external CometD libraries [src8] |
| Pub/Sub API Managed Subscriptions | Spring '24 (v60.0) | Beta | Server-managed subscription state | Salesforce tracks replay position — simplifies client [src2] |
| High-Volume PE as default | Spring '23 (v57.0) | GA | All new PE definitions are high-volume | Standard-volume creation disabled [src1] |
| CDC | Winter '19 (v44.0) | GA | Initial GA for selected standard objects | Entity selection limit: 5 without add-on [src3] |
| Streaming API (CometD) | API v21.0 | GA | Long-standing, stable | Not deprecated, but Pub/Sub API is preferred for new projects [src4] |
| Platform Events | Winter '17 (v38.0) | GA | Initial GA | Apex triggers, Flow support, API publishing [src1] |

### Deprecation Policy

Salesforce has not deprecated the Streaming API or CometD-based event consumption. Both Streaming API and Pub/Sub API coexist and can subscribe to the same events. However, Salesforce is investing new features exclusively in Pub/Sub API (managed subscriptions, publishing support, flow control). New projects should use Pub/Sub API. [src4, src8]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| 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 changes on > 5 objects without add-on license | Platform Events with Apex triggers |
| Need guaranteed event delivery with replay (within 72h) | Need event retention > 72 hours | External message 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 with each release. Always verify current limits via the Limits REST API (`/services/data/v62.0/limits`) rather than relying on documented defaults. [src1]
- The Platform Events add-on license changes both the CDC entity limit (removes the 5-object cap) and the delivery allocation (adds +100K/day, shifts to monthly enforcement at 3M/month). Contact Salesforce account team for current pricing. [src1, src3]
- Sandbox orgs have independent event allocations that may be lower than production. Performance testing in sandbox may not accurately reflect production capacity. [src1]
- Pub/Sub API is only available for external connections — Apex code within Salesforce cannot use gRPC. Internal subscribers should use Apex triggers on platform events or empApi in Lightning Web Components. [src5]
- Replay IDs are org-specific and environment-specific. A replay ID from a sandbox cannot be used in production, and vice versa. [src6]

## Related Units

- [Salesforce REST API capabilities and rate limits](/business/erp-integration/salesforce-rest-api/2026)
- [Salesforce Bulk API 2.0 for high-volume data operations](/business/erp-integration/salesforce-bulk-api/2026)
