Salesforce is the CRM backbone for a large proportion of enterprise clients. When you're building a product that needs to integrate with it — whether that's syncing customer data, triggering automation, or reading from a custom Salesforce org — the integration pattern you choose will determine whether your system handles 1,000 records per day or 1,000,000.
We've built Salesforce integrations for clients in fintech, real estate, insurance, and logistics — each with different data volumes and latency requirements. This article covers the three patterns that show up in most enterprise contexts, when each is appropriate, and the specific failure modes that cause integrations to break at scale.
Understanding the Salesforce API Landscape
Before choosing a pattern, you need to understand which Salesforce API you're building against. The main options:
- REST API: Standard CRUD operations on Salesforce objects. 24-hour rate limits based on your org's licence type — typically 1,000 API calls per Salesforce licence, per 24 hours. The most common choice for new integrations.
- Bulk API 2.0: Designed for large data operations — hundreds of thousands to millions of records. Asynchronous, uses CSV or JSON. No per-record API limit; uses a separate "batch" allocation.
- Streaming API (Platform Events / Change Data Capture): Real-time event streaming using a publish/subscribe model. Salesforce pushes changes to your system, rather than your system polling. Best for near-real-time sync.
- SOAP API: Legacy. Avoid for new integrations unless mandated by an existing system.
The Salesforce REST API enforces a concurrent API call limit of 25 long-running requests (over 20 seconds) per org. If your integration fires many synchronous requests in parallel, you'll hit this limit and receive a REQUEST_LIMIT_EXCEEDED error. Design for this from the start.
Pattern 1: Request-Response (Synchronous REST)
The simplest pattern: your application makes a REST call to the Salesforce API and waits for a response. This is appropriate when:
- The user needs immediate confirmation (e.g., creating a lead when a form is submitted and redirecting to a success page)
- Data volume is low — under a few hundred records per day
- Operations are user-triggered rather than system-triggered
The failure mode at scale is straightforward: you exhaust your daily API limit, or your application starts queuing synchronous requests faster than Salesforce can process them, causing request timeouts that surface as errors to your users.
# Python example: creating a Salesforce Lead via REST API
import requests
def create_lead(sf_instance_url: str, access_token: str, lead_data: dict) -> dict:
url = f"{sf_instance_url}/services/data/v59.0/sobjects/Lead/"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
response = requests.post(url, json=lead_data, headers=headers, timeout=10)
response.raise_for_status()
return response.json()
# Usage
lead = create_lead(
sf_instance_url="https://yourorg.my.salesforce.com",
access_token=token,
lead_data={
"FirstName": "Priya",
"LastName": "Sharma",
"Company": "TechCorp",
"Email": "priya@techcorp.io",
"LeadSource": "Web",
}
)
When using synchronous REST, always implement retry logic with exponential backoff, and surface a meaningful error state to users when Salesforce is unavailable — do not let users think their form submission was successful if the API call failed.
Pattern 2: Asynchronous Queue (Decoupled)
For higher-volume integrations or operations that don't require immediate user feedback, decoupling your application from the Salesforce API with a message queue is the correct pattern. Your application writes to a queue (SQS, RabbitMQ, Pub/Sub) and a separate worker process consumes the queue and makes the API calls.
"The moment you decouple the user action from the API call, you gain control over rate limiting, retry behaviour, and failure handling — none of which are possible in a synchronous integration."
This pattern handles API limits gracefully: the worker can pace its consumption of the queue to stay within limits, retry failed requests with backoff, and alert on persistent failures without any of this complexity leaking into your application layer.
# Worker pattern: consuming a queue and writing to Salesforce
import boto3
import json
import time
from simple_salesforce import Salesforce
sqs = boto3.client('sqs', region_name='ap-south-1')
QUEUE_URL = 'https://sqs.ap-south-1.amazonaws.com/123456789/salesforce-sync'
sf = Salesforce(
username=SF_USERNAME,
password=SF_PASSWORD,
security_token=SF_SECURITY_TOKEN,
)
def process_message(message_body: dict):
object_type = message_body['object']
data = message_body['data']
if object_type == 'Lead':
sf.Lead.create(data)
elif object_type == 'Contact':
sf.Contact.upsert(f"External_ID__c/{data['external_id']}", data)
def run_worker():
while True:
response = sqs.receive_message(
QueueUrl=QUEUE_URL,
MaxNumberOfMessages=10,
WaitTimeSeconds=20, # long polling
)
messages = response.get('Messages', [])
for msg in messages:
body = json.loads(msg['Body'])
process_message(body)
sqs.delete_message(
QueueUrl=QUEUE_URL,
ReceiptHandle=msg['ReceiptHandle'],
)
time.sleep(0.5) # ~2 API calls/second = 7,200/hour
Pattern 3: Change Data Capture (Event-Driven Sync)
If your integration needs to react to changes in Salesforce — rather than pushing data into it — Salesforce's Change Data Capture (CDC) is the right tool. CDC publishes a change event on a Salesforce channel whenever a record is created, updated, deleted, or undeleted. Your system subscribes to those events via the Streaming API (using CometD or the newer gRPC-based Pub/Sub API).
This eliminates polling entirely. Instead of your system asking Salesforce "has anything changed?" every N minutes (expensive in API calls, and inherently delayed), Salesforce tells your system the moment something changes.
// Node.js CDC listener using jsforce
const jsforce = require('jsforce');
const conn = new jsforce.Connection({
loginUrl: 'https://login.salesforce.com',
});
await conn.login(SF_USERNAME, SF_PASSWORD);
// Subscribe to Account change events
conn.streaming.topic('/data/AccountChangeEvent').subscribe((message) => {
const { ChangeEventHeader, Name, AnnualRevenue } = message.payload;
const { changeType, recordIds } = ChangeEventHeader;
console.log(`Account ${changeType}: ${recordIds[0]}`);
// Trigger your downstream sync logic here
});
CDC event delivery is retained for 3 days in Salesforce, so if your subscriber goes offline, it can replay missed events using a stored replay ID. Build this replay mechanism into your subscriber from the start.
Common Failure Modes at Scale
These are the integration failures we see most frequently in production:
1. Hitting the 24-Hour API Limit
Monitor your API usage in the Salesforce Developer Console (Setup →︎ System Overview) or via the /services/data/vXX.0/limits endpoint. Set up an alert when you reach 80% of your daily limit. If you're consistently near the limit, you need to move more operations to Bulk API.
2. Ignoring Salesforce's Asynchronous Bulk Job States
Bulk API 2.0 jobs are asynchronous. You submit a job, poll for completion, then retrieve results. Teams that fire and forget bulk jobs end up with silent data failures. Always poll to completion and log failed records.
3. Not Handling Partial Failures
Salesforce REST API can return a 200 OK response for a batch operation where some individual records failed. Always inspect the response body for "success": false on individual records, not just the HTTP status code.
4. Schema Drift
Salesforce admins regularly add, rename, and delete custom fields. Your integration will break silently if a field it depends on is renamed. Build field-level error handling, and establish a change management process with the Salesforce admin team before you go live.
Choosing the Right Pattern
- User-triggered, low volume, needs confirmation: Synchronous REST
- High volume, or system-triggered, or tolerance for eventual consistency: Async queue + worker
- Need to react to Salesforce changes in your external system: Change Data Capture
- Bulk data migration or initial sync: Bulk API 2.0
Most production enterprise integrations end up using multiple patterns in combination — synchronous REST for user-facing operations and CDC or queued workers for system-to-system sync. The important thing is choosing each pattern deliberately rather than defaulting to the simplest approach for all cases.
If you're scoping a Salesforce integration for your product and want to talk through the architecture, get in touch.