Batch Processing 1M Records in Apex: Avoiding Mixed DML and Heap Errors
Process 1 million Salesforce records with Database.Batchable using 2,000-record chunks to avoid heap limits and prevent MIXED_DML_OPERATION errors through careful object separation.
11 Oct 2026, 15:55 UTC

The Problem: Processing 1 Million Records Without Hitting Limits
When you need to process large volumes of Salesforce records in a single transaction, you hit governor limits fast. A single Database.executeBatch() call can't handle millions of records—Salesforce will throw LIMIT_EXCEEDED errors. Worse, mixing DML operations on standard and setup objects triggers MIXED_DML_OPERATION. How do you process a million records reliably while staying within Apex's strict execution boundaries?
The Solution: Chainable Batchable Jobs with Proper Chunking
Implement Database.Batchable with Database.Stateful to maintain state across chunks. The start() method returns a Database.QueryLocator for scalable processing of millions of records. Each batch chunk gets fresh governor limits: 50,000 DML rows, 10,000 SOQL rows, 100 callouts. Chain batches using System.enqueueJob() for complex pipelines.
Concrete Example: Safe Account and Custom Object Processing
This batch processes Account records (standard object) and updates a custom Custom_Data__c object without mixed DML errors. It uses 2,000-record chunks to stay under heap limits and chains to a second batch for cleanup.
public class AccountDataProcessor implements Database.Batchable<SObject>, Database.Stateful {
private Integer recordCount = 0;
private List<Custom_Data__c> customRecords = new List<Custom_Data__c>();
public AccountDataProcessor() {
// Constructor runs in initial context
}
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Name, Industry FROM Account WHERE Industry = 'Technology'
]);
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
// Process up to 2,000 records per chunk
for (Account acc : scope) {
Custom_Data__c cd = new Custom_Data__c(
Account__c = acc.Id,
Processed_Date__c = Date.today()
);
customRecords.add(cd);
}
// DML on custom object only - no mixed DML
if (!customRecords.isEmpty()) {
Database.SaveResult[] results = Database.insert(customRecords, false);
customRecords.clear();
}
recordCount += scope.size();
}
public void finish(Database.BatchableContext bc) {
// Chain to cleanup batch
System.enqueueJob(new CleanupBatch());
}
}
// Chainable cleanup batch
public class CleanupBatch implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id FROM Custom_Data__c WHERE Processed_Date__c = TODAY
]);
}
public void execute(Database.BatchableContext bc, List<Custom_Data__c> scope) {
Database.delete(scope, false);
}
public void finish(Database.BatchableContext bc) {
// Log completion
System.debug('Cleanup completed for ' + scope.size() + ' records');
}
}
// Execute the batch
AccountDataProcessor batch = new AccountDataProcessor();
Database.executeBatch(batch, 2000);
Key Trade-offs and Limitations
Chunk size matters: Use 2,000 records per batch to avoid heap limit exceptions. Larger chunks may work but risk System.LimitException errors. Concurrency limits: Salesforce allows maximum 5 concurrent batches per org. Queue more jobs with System.enqueueJob() but monitor AsyncApexJob records to avoid hitting the 100,000 jobs per 24 hours limit for most editions. Mixed DML avoidance: Never perform DML on both standard and setup objects in the same transaction. Use separate batches or Database.insert() with false for partial success handling.
Verification: Monitor and Test Your Batches
After execution, verify success via Setup > Async Apex Jobs. Look for: Status = 'Completed', Number of Batches matching your record count divided by chunk size, and Errors = 0. Test locally with mock data to confirm state persistence and chunk processing. Query SELECT Id, Status, NumberOfJobs, TotalJobItems FROM AsyncApexJob WHERE Id = 'your_batch_job_id' for programmatic verification.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.