---
title: "Callout limits, and the patterns around them"
url: "https://trailassembly.com/apex-callout-limits-patterns/"
topic: "Apex and Development"
published: "2026-09-15T04:37:55+00:00"
updated: "2026-10-06T13:55:32+00:00"
---

# Callout limits, and the patterns around them

**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.

```apex
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.

```apex
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](https://trailassembly.com/uncommitted-work-pending/) 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](https://trailassembly.com/apex-choosing-async/)** 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.

```apex
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](https://trailassembly.com/apex-batch-that-scales/) 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](https://trailassembly.com/1gp-vs-2gp/) 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.

---

Source: https://trailassembly.com/apex-callout-limits-patterns/
Trail Assembly — independent Salesforce write-ups.
