Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Governor limits

System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

You did a DML statement and then tried to make an HTTP callout in the same synchronous transaction. The platform forbids this because the callout could take a long time and leave the database row locks open. Move the callout to `@future(callout=true)` or a queueable.

Also seen asYou have uncommitted work pending·Please commit or rollback before calling out·uncommitted work pending callout

This rule sounds arbitrary until you understand what's behind it. When Apex does a DML statement, the platform takes row-level locks on the touched records. Those locks persist for the rest of the transaction. If you then make a slow HTTP callout, the locks stay held for as long as the remote server takes to respond — potentially minutes. Other users sit waiting.

The platform's solution: forbid callouts after DML in synchronous code, period.

What triggers it

public static void doStuff(Account a) {
    a.Last_Synced__c = System.now();
    update a;          // DML — takes a lock

    HttpRequest req = new HttpRequest();
    req.setEndpoint('https://api.example.com/sync');
    req.setMethod('POST');
    new Http().send(req);   // 💥 You have uncommitted work pending
}

The order matters. Callout first, DML after = fine. DML first, callout after = blocked.

Fix 1: Reorder — callout first, then DML

If your business logic allows it, just flip the order:

HttpResponse res = new Http().send(req);
if (res.getStatusCode() == 200) {
    a.Last_Synced__c = System.now();
    update a;
}

This works only when the DML doesn't gate the callout (i.e., the callout doesn't depend on a value you wrote in the DML). For most "sync this record to an external system" cases, that's fine.

Fix 2: Move the callout into @future(callout=true)

The future call runs in a separate transaction with no inherited locks.

public class ExternalSync {
    @future(callout=true)
    public static void syncAccount(Id accountId) {
        Account a = [SELECT Id, ... FROM Account WHERE Id = :accountId];
        HttpRequest req = ...;
        new Http().send(req);
    }
}

// Caller
update a;
ExternalSync.syncAccount(a.Id);   // queues an async callout

The downside: you can't @future from @future (see AsyncException: Future method cannot be called from a future or batch).

Fix 3: Queueable Apex

Queueables are the more flexible primitive — they can chain themselves and return a job ID. You can implement Database.AllowsCallouts to enable callouts:

public class ExternalSyncJob implements Queueable, Database.AllowsCallouts {
    public Id accountId;
    public ExternalSyncJob(Id id) { this.accountId = id; }
    public void execute(QueueableContext ctx) {
        // callout + DML are both fine here
    }
}

System.enqueueJob(new ExternalSyncJob(a.Id));

Without Database.AllowsCallouts, the queueable can DML but not callout.

When the rule fires unexpectedly

Sometimes the DML happens in a place you didn't expect — a downstream trigger, a flow, a workflow field update. If your code looks correct and you still hit this error, log all DML before the callout:

System.debug('DML rows so far: ' + Limits.getDmlRows());

If the count is non-zero, something did DML — find it before adding the callout.

A common pattern: don't return early without rolling back

If your code does some DML, then conditionally tries a callout, then conditionally does more DML — and a callout fails — you may want to roll back the first DML. Use Database.setSavepoint() and Database.rollback() at the top of the method to bracket the changes safely.

Related dictionary terms