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.
