100 callouts per transaction and 120 seconds of cumulative callout time. The second one is the ceiling that actually stops people, because a slow endpoint burns it with a handful of requests. Design around the time budget, not the count.
The two numbers
Each transaction gets 100 callouts. That counts HTTP requests and web service calls together, and it applies whether the transaction is synchronous or asynchronous.
Separately, the total time spent in callouts across the transaction is capped at 120 seconds. A single callout also has its own timeout, which you set on the request and which defaults to 10 seconds. The cumulative ceiling is the one that surprises people, because 12 calls to an endpoint that takes 10 seconds each exhausts it.
Callout time does not count against CPU time. A transaction can sit at 115 seconds of callout with almost no CPU consumed, and it will still fail on the callout budget.
Set the timeout explicitly
Always. The default is short enough to cause flaky failures and long enough that a hung endpoint eats your whole budget.
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Billing_API/v2/invoices');
req.setMethod('POST');
req.setTimeout(20000); // milliseconds, max 120000
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(payload));
Http h = new Http();
HttpResponse res = h.send(req);
Set it per call, based on what you know about the endpoint. Six calls at 20 seconds is 120 seconds in the worst case, so if you plan six calls you have already spent the budget on paper. Plan the arithmetic before you write the loop.
Batch the requests, not the transaction
The first instinct when you need 500 records sent is to loop. Do not. Look for the bulk endpoint instead, because one request carrying 500 records costs one callout and one round trip.
List<Map<String, Object>> payload = new List<Map<String, Object>>();
for (Account a : accounts) {
payload.add(new Map<String, Object>{
'externalId' => a.External_Id__c,
'name' => a.Name
});
}
req.setBody(JSON.serialize(payload));
If the API has no bulk endpoint, chunk by whatever it accepts and keep the chunk count in single digits. Asking the vendor for a bulk route is cheaper than the architecture you would otherwise write.
The setCallout and DML ordering rule
You cannot make a callout after uncommitted DML in the same transaction. The error is “You have uncommitted work pending”, and it exists because an open transaction cannot be held while waiting on an external system.
Three ways out:
- Do the callout first, then the DML. Simplest, and works when you do not need a record Id before calling.
- Move the callout to a queueable implementing
Database.AllowsCallouts. The DML commits, then the async job runs and calls out. - Use
@future(callout=true)for the simple case. Queueable is better in almost every respect, including that it can be chained and takes real object parameters.
From a trigger, the callout must be asynchronous. That is not negotiable.
Failure handling that is not optional
External systems return 500s, time out and rate-limit you. Code that assumes 200 is code that will page someone.
if (res.getStatusCode() == 429 || res.getStatusCode() >= 500) {
IntegrationRetry.schedule(recordIds, attempt + 1);
} else if (res.getStatusCode() >= 400) {
IntegrationLog.permanentFailure(recordIds, res.getBody());
}
Distinguish retryable from permanent. A 429 or a 503 should be retried with a delay. A 400 or a 422 means your payload is wrong and retrying it forever is a way to get your API key revoked. Cap the retry count and log the give-up.
Do not retry inside the same transaction. A retry loop with three attempts against a slow endpoint is how you spend 120 seconds on one record.
Spreading work across transactions
When you genuinely need hundreds of calls, the pattern is a queueable that processes a chunk and chains the next:
- Process 20 records, or as many as fit comfortably inside the time budget.
- Check
Limits.getCallouts()against the limit before each call. - Enqueue the next chunk with the remaining Ids, if any remain.
Each link gets a fresh 100 callouts and a fresh 120 seconds. Batch Apex with a small scope and Database.AllowsCallouts does the same job when the source is a query, and gives you the job monitoring for free.
Frequently asked
Do callouts in a managed package count against mine? No, a certified managed package has its own callout allowance under its namespace. Callout time contributes to the transaction either way.
What is the maximum timeout I can set? 120,000 milliseconds, which is the entire cumulative budget for one call. Setting it that high is almost always wrong.
Does a failed callout still count? Yes. A timeout consumes a callout and the time it waited.
Can I make callouts from a batch execute? Yes, if the class implements Database.AllowsCallouts. Each execute is its own transaction with its own budget.
