Mitigating Salesforce API Limit Exhaustion with Reverse ETL and Bulk API 2.0
Enriching Salesforce with behavioral product data, warehouse-modeled traits (Snowflake, BigQuery, Databricks), and marketing touchpoints is central to modern revenue operations. Tools like Census, Hightouch, and custom sync scripts make it trivial to define reverse ETL pipelines that push warehouse rows directly into Account, Contact, or custom objects.
However, sync jobs that write to Salesforce without strict batch governance quickly trigger the dreaded runtime exception: REQUEST_LIMIT_EXCEEDED: TotalRequests Limit exceeded.
Salesforce enforces rolling 24-hour API request allocations based on edition and user licenses. When a reverse ETL pipeline exhausts this quota:
- Critical operational systems (HubSpot syncs, Marketo form handlers, billing gateways) immediately halt, rejecting inbound customer updates.
- Outbound marketing webhooks back up into memory or crash with unhandled queue exceptions.
- Data pipeline syncs fail mid-batch, leaving records partially transformed across your database.
This playbook explains how to diagnose API quota drain, configure Reverse ETL pipelines to leverage Bulk API 2.0 instead of REST/Composite endpoints, and structure change-data-capture (CDC) models to minimize unnecessary API consumption.
Why Standard Reverse ETL Syncs Exhaust API Limits
Salesforce categorizes inbound write operations across distinct API endpoints, each with fundamentally different consumption impacts:
| API Method | Max Records Per Call | API Calls Consumed (for 100,000 Records) | Best Use Case |
Standard REST API (/sobjects/) | 1 record | 100,000 calls | Real-time webhooks, single transactional updates |
Composite SObject Tree (/composite/tree/) | 200 records | 500 calls | Relational hierarchies (Account + Contacts) |
Composite Batch / Graph (/composite/batch/) | 25 subrequests | 4,000 calls | Heterogeneous multi-object payloads |
Bulk API 2.0 (/jobs/ingest/) | Up to 150,000,000 records | 1 call (per 100MB ingested) | High-volume warehouse synchronizations |
The most common architectural mistake is running hourly reverse ETL syncs configured to use the REST or Composite API to achieve near-real-time updates. If a warehouse table syncs 5,000 user activity updates every 30 minutes via standard REST, the sync consumes 240,000 API calls per day—often exhausting the entire 24-hour allocation for an Enterprise edition organization before noon.
Step 1: Force Bulk API 2.0 Ingestion in Reverse ETL
Bulk API 2.0 bypasses the standard per-request API limit calculation. Instead of decrementing your rolling daily call quota for each record or mini-batch, Salesforce evaluates Bulk API 2.0 through a dedicated limit: 15,000 ingest jobs per rolling 24-hour period, with each job capable of handling up to 150 million records or 100MB of CSV data.
Configuration in Reverse ETL Tools (Hightouch / Census):
- Navigate to your Salesforce destination connection settings.
- In the sync configuration engine, locate Sync Engine / Ingestion Method.
- Change the method from
REST API / Automaticto Bulk API 2.0. - Adjust the Sync Batch Size threshold: set the trigger to fire only when either 500+ records have mutated or a maximum time window of 15 to 30 minutes has elapsed.
This shift replaces thousands of discrete REST calls with a single Bulk API ingestion envelope.
If your marketing stack also pushes these events to ad platforms, ensure your offline deal milestones align with the deduplication techniques in our playbook on deduplicating Meta CAPI conversions from HubSpot and Salesforce deals.
Step 2: Implement Change Data Capture (CDC) at the SQL Modeling Layer
A major cause of unnecessary API drain is syncing full warehouse snapshots rather than delta changes. If your SQL query selects all active users, the reverse ETL engine must evaluate every single row, generating lookup hits against Salesforce external IDs.
Instead, build an incremental staging model in your data warehouse (dbt, Snowflake, or BigQuery) that only outputs rows where properties have genuinely mutated:
SQL
-- dbt / Snowflake Incremental Sync Model
{{ config(
materialized='incremental',
unique_key='salesforce_contact_id'
) }}
WITH source_data AS (
SELECT
contact_id AS salesforce_contact_id,
user_email,
workspace_plan_tier,
seats_consumed,
last_active_timestamp,
updated_at
FROM {{ ref('fct_product_usage_daily') }}
)
SELECT
s.salesforce_contact_id,
s.user_email,
s.workspace_plan_tier,
s.seats_consumed,
s.last_active_timestamp
FROM source_data s
{% if is_incremental() %}
-- Only extract records modified since the last successful sync run
WHERE s.updated_at > (
SELECT COALESCE(MAX(updated_at), '1970-01-01')
FROM {{ this }}
)
-- Filter out zero-impact churn: only sync if active metrics shifted
AND (
s.seats_consumed != {{ this }}.seats_consumed
OR s.workspace_plan_tier != {{ this }}.workspace_plan_tier
)
{% endif %}
By filtering out rows where data hasn’t shifted, you reduce sync payload volume by 80% to 95%, keeping your Bulk API queue clear for time-sensitive tasks.
Step 3: Scripted Fallback with Composite SObject Collections
For scenarios where updates must reflect within seconds (such as high-priority demo form routing) and Bulk API 2.0 batch delays are unacceptable, never revert to single-record REST endpoints. Use the Composite SObject Collections API (PATCH /services/data/v60.0/composite/sobjects), which accepts up to 200 records in a single API call:
JavaScript
// Node.js Batch Patch Worker
const axios = require('axios');
async function batchUpdateSalesforceRecords(recordsChunk, sfAccessToken, instanceUrl) {
// Enforce the 200-record hard boundary of Composite SObjects API
if (recordsChunk.length > 200) {
throw new Error('Composite Collections payload exceeds maximum 200-record threshold.');
}
const endpoint = `${instanceUrl}/services/data/v60.0/composite/sobjects`;
const payload = {
allOrNone: false, // Prevents a single validation error from failing the entire batch
records: recordsChunk.map(item => ({
attributes: { type: 'Contact' },
id: item.contactId,
Product_Tier__c: item.tier,
Active_Seats__c: item.seats,
Telemetry_Last_Sync__c: new Date().toISOString()
}))
};
try {
const response = await axios.patch(endpoint, payload, {
headers: {
Authorization: `Bearer ${sfAccessToken}`,
'Content-Type': 'application/json'
}
});
// Inspect individual record statuses within the single-call response
response.data.forEach((result, idx) => {
if (!result.success) {
console.error(`Record ${recordsChunk[idx].contactId} failed:`, result.errors);
}
});
return response.data;
} catch (error) {
console.error('Batch patch call rejected:', error.response?.data || error.message);
throw error;
}
}
This updates 200 Contacts while consuming exactly 1 API call against your daily limit, delivering the low-latency response of REST without exhausting execution budgets.
If your architecture syncs Custom Objects with complex relationship maps, review our guide on resolving HubSpot Custom Object sync latency and routing delays to prevent downstream indexing lag.
Frequently Asked Questions
How can I monitor Salesforce API consumption programmatically?
Inspect the HTTP response headers on any standard REST request. Salesforce returns two diagnostic headers: Sforce-Limit-Info: api-usage=18402/100000, which reports your current 24-hour consumption alongside your hard limit. You can also query the /services/data/v60.0/limits endpoint to retrieve exact quotas across all Salesforce services.
What happens to active integrations when the daily API limit is reached?
Any external request attempting to authenticate or push data receives an HTTP 403 Forbidden response with the error code REQUEST_LIMIT_EXCEEDED. Outbound messages originating from Salesforce continues to function, but inbound data flows (form submits, webhook captures, data syncs) fail immediately until the 24-hour rolling window frees up capacity.
Does Bulk API 2.0 count toward the standard rolling 24-hour API limit?
No. Bulk API 2.0 operates under its own distinct ceiling: a limit of 15,000 ingest jobs per rolling 24 hours. The individual records processed within those jobs do not decrement your standard REST/SOAP API allocation.