Skip to main content

Salesforce Change Data Capture (CDC) to Kafka

Salesforce Change Data Capture (CDC) publishes a change event to the Salesforce event bus every time a record is created, updated, deleted, or undeleted. Streaming those events into Apache Kafka gives you a near-real-time replica of your CRM data for analytics, microservices, caches, and search indexes — without polling the REST API.

This page explains how CDC works and how the Salesforce Kafka connector consumes it.

How Salesforce CDC works​

  1. You enable CDC per object under Setup → Change Data Capture (standard and custom objects).
  2. Every committed DML change publishes a change event to a channel:
    • /data/<Entity>ChangeEvent — one entity (e.g. /data/AccountChangeEvent)
    • /data/ChangeEvents — all selected entities combined
    • /data/<Name>__chn — a custom channel you compose
  3. Subscribers consume the channel via the Pub/Sub API (gRPC + Avro, recommended) or the legacy Streaming API (CometD).
  4. Events are retained in the event bus for 72 hours; each carries an opaque replayId so a subscriber can resume exactly where it stopped.

Anatomy of a change event​

Every event contains the changed fields plus a ChangeEventHeader:

Header fieldMeaning
changeTypeCREATE, UPDATE, DELETE, UNDELETE (+ GAP_*, GAP_OVERFLOW)
entityNameThe SObject, e.g. Account
recordIdsAffected record Ids (bulk DML can carry several)
commitTimestamp / commitNumberTransaction commit position
transactionKeyGroups events from one transaction
changedFields / nulledFieldsBitmap-encoded field lists (the connector decodes these to names)

Two behaviours surprise most first-time consumers:

  • UPDATE events carry only the changed fields — everything else is null. Consumers either merge deltas, or the connector can fetch the full post-image per update (sf.full.record.on.update=true, at the cost of one REST call per update).
  • Gap events carry no field data at all. When Salesforce can't render a normal event (oversized events, internal errors, direct database changes), it sends a GAP_* event with just the header. The connector automatically re-queries those records via REST and re-emits them; GAP_OVERFLOW triggers an incremental Bulk re-sync.

Streaming CDC into Kafka​

With the source connector, each object's change stream lands on its own Kafka topic with the record Id as the key:

{
"connector.class": "sh.oso.salesforce.source.SalesforceSourceConnector",
"sf.sobjects": "Account,Contact,Opportunity",
"sf.topic.prefix": "salesforce",
"sf.snapshot.enabled": "true",
"sf.realtime.mode": "event_driven"
}
  • salesforce.Account, salesforce.Contact, … receive ordered change records with _EventType (created/updated/deleted) and decoded CDC metadata in headers.
  • Enable sf.snapshot.enabled to backfill history first — the connector's snapshot handoff guarantees no gap and no duplicates between the backfill and the live stream.
  • Enable sf.emit.tombstone.on.delete for log-compacted topics that should forget deleted records.

CDC vs the alternatives​

ApproachLatencyAPI costDeletes visible?History
CDC via Pub/Sub APISecondsMinimal (gRPC stream)Yes (incl. undelete)72h replay
Polling with SystemModstamp (Bulk 2.0)Poll interval1 query job per pollOnly with queryAllCursor-based
PushTopics (legacy Streaming API)SecondsLowYes24h replay, being retired

Use CDC wherever it's available; the connector's polling mode covers objects that don't support change events, and the legacy CometD connector covers orgs without Pub/Sub API access.

Operational notes​

  • Allocations: orgs have daily CDC event allocations (edition-dependent; add-ons raise them). Monitor delivery usage in Setup if you enable many high-churn objects.
  • Bulk loads: pausing CDC-heavy pipelines during mass DML avoids flooding the event bus; the connector's snapshot/resync path is the better tool for backfills.
  • Downtime: restarts inside 72 hours replay from the stored replayId; longer outages trigger the connector's gap recovery.