Skip to main content

HTTP Callouts in Batch Apex: Database.AllowsCallouts, Limits & Best Practices

In plain words: Batch Apex with HTTP Callouts allows Salesforce to sync thousands of records with external systems (such as ERPs, payment gateways, or external databases) in manageable chunks. By adding the marker interface 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.AllowsCallouts Marker Interface: Implemented alongside Database.Batchable<sObject>, this interface notifies the Apex runtime that each batch execute() 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.
360 Batch Callout Architecture Card:
  • 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.

Step 1: Write the Batchable Apex Class
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.

Step 2: Execute the Batch via Anonymous Apex or Schedulable Job
// 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

Governor Limit Trap: Default Batch Size (200 Records) and Callout Exceptions
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).
Core Rule: Implement Database.AllowsCallouts, explicitly limit your executeBatch scope size to 100 or fewer, and perform all DML updates after completing the callout loop to prevent uncommitted work pending exceptions.
  • Avoid "Uncommitted Work Pending" Errors: Never execute DML statements (such as insert or update) 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: Implement Database.Stateful on 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.