Most Salesforce integrations work fine in the demo. They work fine in UAT. Then six months after go-live, volume triples, and the same integration starts throwing CalloutException errors in production at 2am. Nobody changed the code. The limits were always there. The traffic just caught up to them.
I’ve inherited more than one of these. The pattern is always the same: a callout was built to solve today’s problem, with no thought for what happens when the record count goes from hundreds a day to tens of thousands. Fixing it after the fact costs far more than designing around the limits up front.
Where the limits actually bite
Salesforce’s governor limits around callouts aren’t obscure. They’re published, they haven’t moved much in years, and almost nobody designs for all of them at once.
- 100 callouts per transaction, synchronous or asynchronous combined.
- A 120-second cumulative timeout per transaction for all callouts.
- 6MB heap in a synchronous transaction, 12MB in asynchronous.
- A daily API request limit per org, scaled by edition and licence count, that counts every REST, SOAP, and Bulk API call against the same bucket.
None of these are hard to hit. A trigger that calls out once per record, running on a batch of 200 from a Data Loader import, blows past the 100-callout ceiling before lunch. A nightly sync job that pulls every modified record one at a time, instead of in bulk, burns through the daily API allocation while the rest of the org is still waking up.
The patterns that actually hold up
Bulkify the callout, not just the trigger
Everyone bulkifies DML by now. Fewer people apply the same discipline to callouts. If a trigger fires on 200 records and the external system has a batch endpoint, send one request with 200 records in the payload, not 200 requests. Most modern integration platforms and even plenty of legacy systems support this. If the other side genuinely can’t take bulk payloads, that’s a conversation to have with whoever owns that system, not a problem to absorb silently into Apex.
Go async before you’re forced to
Queueable Apex, Batch Apex, and scheduled jobs exist precisely because synchronous transactions have the tightest limits. A callout triggered from a user clicking a button needs to happen fast or not at all from their perspective. A callout syncing records to a data warehouse doesn’t. Moving it to Queueable buys a separate governor limit context, more heap, and room to retry without holding up the user. The habit worth building: ask whether a callout needs to block the transaction it’s in, and default to async when the answer is no.
Treat the daily API limit as a budget, not a wall
Teams tend to notice the API limit only once they’ve hit it, which is the wrong time to notice. Set up an alert well before the ceiling, at 70 or 80 percent of daily usage, using the REST API Usage metric or a scheduled check against OrgLimits. Then look at what’s actually consuming the budget. In a contact-centre org I worked with, a third-party case enrichment tool was polling for updates every five minutes around the clock, whether or not anything had changed. Moving that to an event-driven push cut its API footprint by more than half, and the integration got faster in the process.
What happens when the other side goes down
Design for the happy path and the integration will fail the first time the external system has a bad day, which it will. A few things make that survivable instead of catastrophic:
Retries with backoff, not retries with brute force
Retrying a failed callout immediately, in a loop, is how a flaky external API turns into a self-inflicted denial of service. Exponential backoff, implemented through a scheduled or Queueable retry with increasing delay, gives the other system room to recover instead of hammering it the moment it’s struggling.
Idempotency keys for anything that writes
If a callout can be retried, it will eventually be retried twice for the same operation, whether from a timeout, a network blip, or a scheduled job overlapping with a manual retry. Design the payload with a unique idempotency key and have the receiving system (or a staging layer on this side) reject duplicates. Without this, retry logic that’s meant to make the integration more reliable quietly starts creating duplicate records instead.
A circuit breaker for the integration, not just error handling
When an external system starts failing consistently, stop calling it. A simple circuit breaker, tracking consecutive failures and pausing outbound callouts for a cooldown period once a threshold is crossed, protects both the Salesforce org’s limits and the struggling external system. It also turns a 2am pager alert into a dashboard that says “integration paused, retrying in 10 minutes” instead of a transaction log full of timeouts.
Stop polling where you can push
Platform Events and Change Data Capture exist specifically to replace the polling pattern that eats API allocation for no good reason. If an external system needs to know when a Salesforce record changes, publishing an event the moment it happens is cheaper, faster, and far less fragile than that system asking “anything new?” every few minutes. The same logic runs in reverse: if Salesforce needs near-real-time updates from an external system, check whether it can push a webhook into an inbound integration rather than Salesforce polling it on a schedule. Either direction, push beats poll on cost and on latency.
Named Credentials are doing more than you think
Teams often treat Named Credentials as just a place to store an endpoint and a password. They’re also the natural place to centralise retry-aware timeout settings, and they decouple the authentication mechanism from the Apex code calling it. When an OAuth token needs refreshing or a certificate needs rotating, that change happens in Setup, not in a deployment. For integrations that are going to live for years and get touched by whoever’s on the team at the time, that separation matters more than it looks like it does on day one.
None of this needs to be built on day one for every integration. A low-volume internal tool calling an internal API once a day doesn’t need a circuit breaker. But the moment an integration touches customer-facing volume, or sits on a path that other automation depends on, these patterns stop being nice-to-haves. The cost of adding them after the first production outage is always higher than the cost of designing for scale before the first record ever gets sent.