Database.AllowsCallouts to your batch class and capping your batch execution size to 100 records or fewer, you can make external REST/SOAP requests across millions of records without hitting governor limits.
Synchronous transactions in Salesforce enforce strict timeout and callout limits, making it impossible to synchronize large datasets directly from UI controllers or record-triggered flows. When enterprise systems require bulk synchronization, scheduled data enrichment, or high-volume status verification, Batch Apex provides the scalable, asynchronous backbone needed to process records in controlled chunks over time.
1. Key Architecture: Enabling Callouts in Batch Jobs
By default, Salesforce blocks external HTTP requests from running inside Batch Apex to prevent long-running worker threads from locking resources. To enable callouts, you must configure two foundational elements:
- The
Database.AllowsCalloutsMarker Interface: Implemented alongsideDatabase.Batchable<sObject>, this interface notifies the Apex runtime that each batchexecute()transaction is authorized to initiate outbound HTTP/SOAP traffic. - Governor Limit Boundaries per Batch Chunk: Each chunk in the
execute()method runs as a distinct asynchronous transaction. While you receive fresh governor limits for each chunk, a single transaction can make a maximum of 100 HTTP callouts.
- Mandatory Interface:
implements Database.Batchable<sObject>, Database.AllowsCallouts - Maximum Callouts per Chunk: 100 callouts per
execute()execution block. - Recommended Batch Scope Size:
Database.executeBatch(new MyBatch(), 50);(Must be ≤ 100 if making 1 callout per record). - Authentication Standard: Salesforce Named Credentials & External Credentials (avoids hardcoding endpoints and access tokens).
2. Implementing a Production-Ready Batch Callout Class
The following example demonstrates how to query records in the start() method, make secure REST callouts via Named Credentials inside the execute() loop, and perform bulk updates on the processed records.
public with sharing class AccountSyncBatch implements Database.Batchable<sObject>, Database.AllowsCallouts, Database.Stateful {
// Track total successes and failures across batch chunks
public Integer totalSuccesses = 0;
public Integer totalFailures = 0;
public Database.QueryLocator start(Database.BatchableContext bc) {
// Query records that require external synchronization
return Database.getQueryLocator([
SELECT Id, Name, AccountNumber, Sync_Status__c
FROM Account
WHERE Sync_Status__c = 'Pending'
WITH USER_MODE
]);
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
List<Account> accountsToUpdate = new List<Account>();
Http http = new Http();
for (Account acc : scope) {
HttpRequest req = new HttpRequest();
// Use Named Credentials to handle endpoints and auth automatically
req.setEndpoint('callout:ERP_Integration_Named_Credential/v1/accounts/sync');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(20000); // 20-second timeout per callout
// Build request payload
Map<String, Object> payload = new Map<String, Object>{
'salesforceId' => acc.Id,
'accountName' => acc.Name,
'accountNumber' => acc.AccountNumber
};
req.setBody(JSON.serialize(payload));
try {
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200 || res.getStatusCode() == 201) {
acc.Sync_Status__c = 'Synced';
acc.Last_Sync_Date__c = System.now();
totalSuccesses++;
} else {
acc.Sync_Status__c = 'Failed';
acc.Sync_Error_Message__c = 'HTTP ' + res.getStatusCode() + ': ' + res.getBody();
totalFailures++;
}
} catch (Exception ex) {
acc.Sync_Status__c = 'Failed';
acc.Sync_Error_Message__c = ex.getMessage();
totalFailures++;
}
accountsToUpdate.add(acc);
}
// Commit field updates using user-mode security
if (!accountsToUpdate.isEmpty()) {
update as user accountsToUpdate;
}
}
public void finish(Database.BatchableContext bc) {
System.debug(LoggingLevel.INFO, 'Batch Sync Complete. Successes: ' + totalSuccesses + ', Failures: ' + totalFailures);
}
}
3. Invoking the Batch with Controlled Scope Size
When invoking a batch job that performs one HTTP callout per record, you must set the batch size parameter in Database.executeBatch() to a value equal to or less than 100.
// Instantiate the batch class
AccountSyncBatch batchJob = new AccountSyncBatch();
// Set batch size explicitly to 50 (must be <= 100 to stay within callout limits)
Id batchJobId = Database.executeBatch(batchJob, 50);
System.debug('Enqueued Batch Job ID: ' + batchJobId);
4. Testing Batch Callouts with HttpCalloutMock
Salesforce requires 100% simulated callouts during unit testing. To test your batch class, pair HttpCalloutMock with Test.startTest() and Test.stopTest() to force batch chunks to complete synchronously.
@isTest
private class AccountSyncBatchTest {
// Mock HTTP callout response
private class AccountSyncMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setStatusCode(200);
res.setBody('{"status": "SUCCESS", "erpId": "ERP-98765"}');
return res;
}
}
@testSetup
static void setupTestData() {
List<Account> testAccounts = new List<Account>();
for (Integer i = 0; i < 10; i++) {
testAccounts.add(new Account(
Name = 'Batch Test Account ' + i,
Sync_Status__c = 'Pending'
));
}
insert as user testAccounts;
}
@isTest
static void testBatchExecutionWithCallouts() {
Test.setMock(HttpCalloutMock.class, new AccountSyncMock());
Test.startTest();
AccountSyncBatch batch = new AccountSyncBatch();
Database.executeBatch(batch, 10);
Test.stopTest(); // Forces the batch job to complete all chunks
List<Account> syncedAccounts = [SELECT Id, Sync_Status__c FROM Account WHERE Sync_Status__c = 'Synced'];
Assert.areEqual(10, syncedAccounts.size(), 'All 10 test accounts should be marked as Synced');
}
}
5. Common Traps & Architectural Best Practices
Executing
Database.executeBatch(new AccountSyncBatch()); without specifying a size defaults the scope to 200 records. If your execute() method makes an HTTP callout for each record, the 101st record will immediately throw an unrecoverable System.LimitException: Too many callouts: 101, causing the entire chunk to roll back. Always pass an explicit scope size (≤ 100).
- Avoid "Uncommitted Work Pending" Errors: Never execute DML statements (such as
insertorupdate) before making an HTTP callout in the same transaction. Always make your callouts first, collect the responses, and execute your DML operations at the end of the method. - Use Named Credentials: Replace hardcoded endpoint strings and authorization tokens with Salesforce Named Credentials to streamline certificate handling and token refreshes.
- Track Progress with
Database.Stateful: ImplementDatabase.Statefulon your batch class if you need to aggregate counters, error summaries, or success logs across multiple chunk executions.
Summary
Combining HTTP callouts with Batch Apex gives Salesforce developers an enterprise-grade pattern for bulk external data integration. By pairing Database.AllowsCallouts with disciplined batch sizing (≤ 100 records), Named Credentials, and structured error handling, organizations can safely process high-volume integrations without risking governor limit violations.