The best way to run bidirectional sync with Salesforce is two one-way flows built as a pair. Change Data Capture streams changes out of Salesforce, and upserts by external ID write changes back in. Every write you make carries a client ID so you can ignore its echo, each field has one owning system, and a scheduled reconciliation job catches what the event stream misses. No single Salesforce feature does all of that, and Salesforce's own integration guide describes sync one direction at a time.
This guide is for SaaS founders, CTOs and product leaders whose product has to sync both ways with each customer's Salesforce org. That is a customer-facing integration. You design it once, then it runs in hundreds of orgs with their own fields, automation and limits. Appnigma helps B2B SaaS teams build and maintain exactly this. With our done for you service, the promise is "Build your product. We'll build the integrations." Teams whose own engineers want to build can use our integration infrastructure instead. If you are still deciding who should build the sync at all, start with our build vs buy guide for bidirectional sync.
Updated October 2026. Checked against Salesforce's Integration Patterns and Practices guide, the Change Data Capture and Pub/Sub API developer guides, and the REST API and limits documentation. The earlier version of this post called middleware "the BEST option for 90% of businesses" and said iPaaS tools "handle conflict resolution", with no source for either. It listed Change Data Capture as an API and gave no mechanism for stopping update loops. Its five reference links were never added. This update replaces all of that with the documented Salesforce mechanisms and their limits.
Which Salesforce mechanism to use for each direction
Two-way sync is really two integrations that share keys. Salesforce's Batch Data Synchronization pattern rates Change Data Capture as "Best" when Salesforce is the data master, and it marks near-real-time call-in for sync as "Suboptimal" because it "has the potential for causing continual updates." The Pattern Summary is just as plain that a batch design is valid in both directions, saying "Updates in either direction are done in a batch manner." Real time is a choice, not a requirement.
| Direction | Mechanism | Fits when | Documented limit to plan around |
|---|---|---|---|
| Salesforce to your product | Change Data Capture over Pub/Sub API | You need record changes within seconds | 25,000 delivered events per 24 hours on Enterprise Edition, shared by every subscriber |
| Salesforce to your product | Platform events | Customer automation should decide what to send | Published once, "There is no retry on the Salesforce side" |
| Salesforce to your product | Outbound messages | A customer admin already uses them | Dropped after 24 hours, can arrive out of order or twice |
| Salesforce to your product | Polling on SystemModstamp | Small orgs, or as the reconciliation pass | Each poll spends the customer's daily API calls |
| Your product to Salesforce | REST upsert by external ID | Single records as they change | One API call per record |
| Your product to Salesforce | sObject Collections upsert | Steady volume of changes | Up to 200 records, one call |
| Your product to Salesforce | Bulk API 2.0 upsert | Initial loads and large backfills | Async jobs, still counted in the daily allocation |
The platform event row comes from the Fire and Forget pattern. The outbound message limits come from Understanding Notifications, which says messages "can be delivered out of order" and can "sometimes be delivered more than one time." Our comparison of platform events and Change Data Capture goes deeper on the event side.
Reading changes out of Salesforce with Change Data Capture
Change Data Capture (CDC) is the Salesforce feature that publishes an event for each record create, update, delete and undelete. Per Change Event Object Support, events cover "all custom objects defined in your Salesforce org and a subset of standard objects." Subscribe with Pub/Sub API. The Streaming API overview says "we recommend you use Pub/Sub API instead of Streaming API," and two header fields you need for updates, nulledFields and diffFields, are "Not available in CometD."
Three properties of the stream decide your design. First, retention. Per Event Message Durability, "Salesforce stores platform events and change data capture events for 72 hours." Store the replay ID of the last event you processed and resubscribe from it after an outage. Past 72 hours, the only way back is a full resync. The same page warns that replay IDs "aren't guaranteed to be contiguous," so never treat a jump as lost data.
Second, transactions. Salesforce recommends that you "replicate all the changes in one transaction as a single commit in your system," per Transaction-Based Replication Steps. The transactionKey and sequenceNumber header fields let you group and order them. Apply an Opportunity and its line items together, or your product briefly shows a deal with no products.
Third, matching keys. A change event carries the Salesforce record ID, but your product may key records on its own ID. Event enrichment exists for this. Salesforce says to "use enrichment when your app needs an external ID field for matching records in an external system," and you can add up to 10 enriched fields per channel member.
Writing changes into Salesforce without creating duplicates
Every write into a customer's org should be an upsert keyed on an external ID field that holds your product's record ID. The Remote Call-In pattern puts this work on your side. It says "the remote system (or middleware) manages error handling and idempotent design." Upsert gives you idempotency for free. A retried request matches the record it created the first time and updates it.
PATCH /services/data/v67.0/sobjects/Account/Acme_Customer_Id__c/C-10293
Sforce-Call-Options: client=acme-sync
Content-Type: application/json
{"Name": "Northwind Traders", "Plan__c": "Enterprise"}The Sforce-Call-Options header is what makes loop prevention possible later, so set it on every request. Know the three outcomes. No match creates a record and one match updates it. Per Upsert Records Using sObject Rows by External ID, "If the external ID matches multiple existing records, then a 300 error is returned, and no records are created or updated." Mark the external ID field Unique so the 300 can't happen.
For volume, sObject Collections upserts "up to 200 records based on an external ID field" in one call, and Bulk API 2.0 takes over for initial loads. The Remote Call-In guide calls any operation over 2,000 records "a good candidate for Bulk API 2.0." Our Bulk API 2.0 guide covers job sizing.
Expect the customer's org to reject some writes your test org accepted. A customer's duplicate rule set to block stops the record. The Duplicate Rule Header option allowSave only helps when the rule is set to alert. Route those rejections to a queue the customer's admin can see, not to a silent retry.
How to stop echo loops and settle conflicts
An echo loop happens when your own write comes back to you as a change event. Salesforce built the fix into the event header. The changeOrigin field on ChangeEventHeader Fields exists so you can "detect whether your app initiated the change to not process the change again." It only names your app if the write set a client ID, so every write must send the header Sforce-Call-Options with a value such as client=acme-sync. Your subscriber then skips any event whose changeOrigin contains client=acme-sync.
Skip the echo, but not blindly. A customer's Flow or trigger can change other fields in the same transaction as your write. Compare changedFields against the fields you wrote, and pass through any field you didn't write.
Conflicts are the second problem. Salesforce describes ownership per entity, saying it "is the system of record for certain entities" in the Batch Data Synchronization pattern. For two-way sync, our advice is to go one level finer and give every synced field one owner. Your product owns usage and plan fields, the customer's reps own stage and amount, and neither side ever overwrites the other's fields. Where a field truly has two writers, check before you write. Conditional request headers let you send If-Unmodified-Since, and Salesforce rejects the update with a 412 if the record changed after that time. Our build vs buy guide walks through a 412 rule set in more detail.
Is Change Data Capture enough for bidirectional sync?
No. CDC covers the steady state, and Salesforce says so itself. The Batch Data Synchronization pattern says "Change Data Capture takes care of the continuous synchronization part of replication." Four gaps need a second path.
The first gap is the initial load. Events only cover changes after you subscribe, so every new customer starts with a Bulk API 2.0 extract.
The second is gap and overflow events. A gap event has the record ID but no field values, so re-read the record. An overflow event fires "when a single transaction contains more than 100,000 changes" and carries no record IDs, so Salesforce's replication guidance is "to retrieve all records of the corresponding entity."
The third is hard deletes. Per Change Data Capture General Considerations, deleting a record from the Recycle Bin is among the actions that don't generate change events.
The fourth is outages longer than 72 hours and an exhausted delivery allocation. Both end the same way, with a full resync of the affected objects.
So a production sync runs two paths. The event path handles changes in seconds. A nightly reconciliation compares SystemModstamp values and repairs anything that drifted. Per System Fields, SystemModstamp records changes by "a user or automated process (such as a trigger)," which LastModifiedDate doesn't.
What changes when you sync with every customer's org
Every limit above belongs to the customer's org, not to you. That is the part internal integration guides never cover, and it shapes the architecture more than any tool choice.
Start with the event budget. An Enterprise Edition org gets 25,000 delivered events in 24 hours, per Change Data Capture Allocations. The same page says "If you have multiple client subscribers, your usage is added across all subscribers." Here is a worked example from those numbers. If a customer's org already runs another vendor's subscriber that receives 15,000 events a day, your sync has 10,000 left. That is about 416 events an hour. A single mass update of 12,000 Accounts uses it all, and "the subscription is disconnected." Read usage per org before you promise near real time, and fall back to polling for customers near the ceiling.
The entity limit works in your favor if you ship a managed package. Orgs can select 5 entities for CDC. The same page says "the selections made by the AppExchange package in a custom channel don't count against your org's allocation," for first and second generation packages. That exemption covers entity selections only, not delivered events. Packaging the channel also means the customer's admin doesn't have to set up CDC by hand. That is one reason a native Salesforce app can be easier to roll out than a pure external sync. Our marketplace apps page puts it as "Your product, inside Salesforce or HubSpot."
API calls follow the same rule. API Request Limits and Allocations states that "Limits and allocations are not on a per-user basis." Your writes, your reconciliation queries and every other integration in that org share one daily pool. Read the Sforce-Limit-Info header on each response and slow down before the customer's other tools start failing. Our guide to Salesforce API limits across customer orgs has the full allocation table.
The same per-org view applies to operations. Each customer authorizes your app in their own org, which our OAuth guide for SaaS connectors covers, and each connection fails in its own way. Our integration infrastructure is built around this, with the promise to "Monitor each customer's connections, identify issues" and ship fixes through review. Whoever runs your sync needs a per-customer view of replay position, event usage, API usage and rejected writes.
Mistakes that break a two-way Salesforce sync after launch
These show up after the first real customers, because test orgs have no automation, no other integrations and no history.
- Writing without a client ID in Sforce-Call-Options, so changeOrigin can't separate your changes from the customer's.
- Storing the replay ID before the change is committed in your product, which skips events after a crash.
- Treating a gap event as a normal update and writing empty values over real data.
- Letting both systems own the same field with last write wins, so a rep's edit disappears with no trace.
- Running the initial Bulk API 2.0 load with CDC already subscribed, so every loaded record echoes back as an event and spends the delivery allocation.
- Assuming a test org's limits. A Developer Edition org gets 10,000 delivered events a day, and production customers on different editions get different numbers.
For the vendor side of this decision, see our list of Salesforce two-way sync platforms.
Frequently Asked Questions
Is real-time sync necessary for Salesforce bidirectional sync?
No. Salesforce's Batch Data Synchronization pattern supports updates in either direction in batches. Use CDC only for the objects where minutes of delay would hurt the customer.
How do you prevent infinite loops in Salesforce two-way sync?
Send Sforce-Call-Options with a client value on every write. Then skip change events whose changeOrigin contains that client value.
Does Change Data Capture fire for hard deletes?
No. Records deleted from the Recycle Bin don't generate change events, so a reconciliation job has to catch them.
Can Appnigma build and maintain two-way Salesforce sync for our product?
Yes. Through done for you, a named integration engineer builds the sync with each customer's field mappings and keeps maintaining it.
Related Articles
- Build vs Buy Bidirectional Sync for Salesforce and HubSpot
- Platform Events vs Change Data Capture in Salesforce
- Top Integration Platforms for Salesforce Two-Way Sync
- Salesforce Integration Tools
- Salesforce ERP Integration Guide
Sources
- Salesforce Developers, Integration Patterns and Practices, Batch Data Synchronization
- Salesforce Developers, Integration Patterns and Practices, Pattern Summary
- Salesforce Developers, Integration Patterns and Practices, Remote Process Invocation, Fire and Forget
- Salesforce Developers, Integration Patterns and Practices, Remote Call-In
- Salesforce Developers, SOAP API Developer Guide, Understanding Notifications
- Salesforce Developers, Change Data Capture Developer Guide, Change Event Object Support
- Salesforce Developers, Streaming API Developer Guide, Introducing Streaming API
- Salesforce Developers, Pub/Sub API Developer Guide, Event Message Durability
- Salesforce Developers, Change Data Capture Developer Guide, Transaction-Based Replication Steps
- Salesforce Developers, Change Data Capture Developer Guide, Enrich Change Events with Extra Fields
- Salesforce Developers, REST API Developer Guide, Upsert Records Using sObject Rows by External ID
- Salesforce Developers, REST API Developer Guide, Upsert Records Using sObject Collections
- Salesforce Developers, Bulk API 2.0 Developer Guide, Upsert Records
- Salesforce Developers, REST API Developer Guide, Duplicate Rule Header
- Salesforce Developers, Change Data Capture Developer Guide, ChangeEventHeader Fields
- Salesforce Developers, REST API Developer Guide, Conditional Request Headers
- Salesforce Developers, Change Data Capture Developer Guide, Gap Events
- Salesforce Developers, Change Data Capture Developer Guide, Overflow Events
- Salesforce Developers, Change Data Capture Developer Guide, General Considerations
- Salesforce Developers, Object Reference, System Fields
- Salesforce Developers, Change Data Capture Developer Guide, Change Data Capture Allocations
- Salesforce Developers, Salesforce Developer Limits and Allocations Quick Reference, API Request Limits and Allocations



