Skip to main content

How to Fix "You Have Uncommitted Work Pending" Error in Salesforce Apex

In plain words: The error System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out happens when your code modifies the database (via insert, update, or delete) and then tries to make an external HTTP callout in the same synchronous transaction.

Salesforce blocks this sequence to preserve data integrity and prevent long database row locks while waiting for third-party web services to respond. Understanding why this limit exists—and how to structure your Apex correctly—will help you design reliable, enterprise-grade integrations.

Why Does Salesforce Throw This Error?

In a multitenant database architecture, database transactions must be as fast and short-lived as possible. When you execute a DML statement, Salesforce opens a database transaction and holds database locks on the affected records.

If Salesforce allowed you to make an external HTTP callout while holding those uncommitted locks, an external server delay or timeout could lock records indefinitely. To prevent platform bottlenecks, the Apex runtime halts execution the moment a callout is attempted after pending DML work.

The Golden Sequence Rule: Performing a Callout before DML in the same transaction is fully permitted. Performing a DML before Callout is strictly prohibited.

Common Scenarios That Trigger the Exception

  • DML Preceding a Callout: Inserting a log record or staging record right before sending an HTTP request.
  • Apex Triggers: Attempting a synchronous callout directly from a database trigger (which by definition is already inside an active DML transaction).
  • Loops with Mixed Operations: Updating a record and requesting external API data inside the same iteration.
  • Implicit DML: Running code that implicitly updates records (such as sending an email with saved activity history) before invoking an external service.

Proven Strategies to Resolve the Error

Solution 1: Reorder Operations (Callout First, DML Second)
The cleanest fix for synchronous flows is simply swapping execution order. Make your HTTP request, process the incoming JSON/XML payload, and perform your database inserts or updates last.
// Correct Synchronous Pattern
public void syncAccountWithExternalSystem(String accountId) {
    // 1. Perform Callout FIRST
    HttpRequest req = new HttpRequest();
    req.setEndpoint('https://api.example.com/customers/' + accountId);
    req.setMethod('GET');
    
    Http http = new Http();
    HttpResponse res = http.send(req);

    // 2. Perform DML AFTER callout completes
    if (res.getStatusCode() == 200) {
        Account acc = new Account(Id = accountId, Sync_Status__c = 'Synced');
        update acc;
    }
}
Solution 2: Use Queueable Apex with Database.AllowsCallouts
When you must perform DML first (or when logic initiates from a Trigger), move the callout into an asynchronous Queueable job. This runs the callout in a brand-new, isolated transaction context.
// Queueable Asynchronous Pattern
public class AsyncCalloutQueueable implements Queueable, Database.AllowsCallouts {
    private Id recordId;

    public AsyncCalloutQueueable(Id recordId) {
        this.recordId = recordId;
    }

    public void execute(QueueableContext context) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.example.com/sync');
        req.setMethod('POST');
        req.setBody('{"recordId":"' + this.recordId + '"}');
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        // Log results or update record asynchronously
        update new Account(Id = this.recordId, External_Status__c = res.getStatus());
    }
}
Solution 3: Use Future Methods for Lightweight Needs
For simpler decoupled operations that do not require job chaining or complex sObject parameter passing, annotate your method with @future(callout=true).
// Future Method Pattern
public class ExternalServiceHelper {
    @future(callout=true)
    public static void sendPayloadAsync(String payloadJson, Id targetId) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:My_Named_Credential/endpoint');
        req.setMethod('POST');
        req.setBody(payloadJson);

        HttpResponse res = new Http().send(req);
        update new Opportunity(Id = targetId, Last_Integration_Code__c = res.getStatusCode());
    }
}

Architectural Best Practices

  • Bulkify Across Integrations: Never execute callouts or DML inside iterative loops. Aggregate your payloads, perform bulk callouts when supported, and commit bulk collections in single DML operations.
  • Leverage Platform Events: For event-driven architectures, publish a Platform Event after DML. An asynchronous subscriber trigger can then handle the callout cleanly without transaction conflicts.
  • Use Named Credentials: Pair your callouts with Named Credentials to simplify authentication, avoid hardcoded endpoints, and improve callout security.
  • Separate Concerns: Maintain dedicated integration service classes separate from trigger handlers and domain logic to prevent unexpected DML-callout interleaving.
Takeaway: Never fight the governor limit by attempting hacky rollbacks. Always split your transactions naturally or order them as Fetch (Callout) → Commit (DML).
360 Card: Transaction Pattern Cheat Sheet
  • Synchronous Flow: Callout → DML (Allowed)
  • Synchronous Flow: DML → Callout (Throws Uncommitted Work Pending)
  • Trigger Context: Always delegate callouts to Queueable or @future(callout=true).
  • LWC / Aura Context: Use Apex continuation or chain client-side JavaScript promises to separate UI saves from callouts.