Batch Apex: Efficiently Archiving Millions of Accounts Without Governor Limits
When you need to move millions of old Account records out of the active database, Batch Apex lets you break the work into chunks, stay under governor limits, and keep the org responsive. Learn how to write, run, and monitor a batch that archives inactive accounts in Salesforce.
01 Apr 2026, 22:59 UTC

Problem: Large‑Scale Record Migration and Governor Limits
Salesforce’s 100‑MByte governor limit and the 10,000‑DML‑row limit per transaction mean that a single synchronous Apex method cannot move millions of records. If you try to delete or archive 5 million Accounts in one go, the job will hit limits before it finishes, leaving the org in an inconsistent state.
Batch Apex was introduced to solve exactly this scenario: it splits a large dataset into a series of smaller, independent transactions that each stay well below governor limits while still running asynchronously.
Why Batch Apex Is the Right Tool for Archiving
Batch Apex implements the Database.Batchable interface, giving you three hooks:
start– generates a query or collection that represents the records to process.execute– runs for each batch of records, performing DML or other work.finish– runs once after all batches complete, useful for notifications or cleanup.
Because each execute call runs in its own transaction, you can safely perform large DML operations without hitting limits. The framework also handles retries and status tracking automatically.
Writing a Batch Class: Archiving Inactive Accounts
Implement Database.Batchable
Below is a minimal, production‑ready example that moves Accounts older than five years into an Archive__c custom object. Replace the placeholder query and field names with those that match your org.
global class ArchiveOldAccountsBatch implements Database.Batchable<SObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
// Query only Accounts older than 5 years that are not already archived
String query = 'SELECT Id, Name, CreatedDate FROM Account WHERE CreatedDate < :Date.today().addYears(-5) AND Archived__c = false';
return Database.getQueryLocator(query);
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
List<Archive__c> archives = new List<Archive__c>();
List<Account> toDelete = new List<Account>();
for (Account a : scope) {
archives.add(new Archive__c(
Name = a.Name,
Original_Id__c = a.Id,
Archived_Date__c = Date.today()));
toDelete.add(a);
}
// Bulk DML – 2 separate statements to keep each within 10,000 rows
if (!archives.isEmpty()) {
insert archives;
}
if (!toDelete.isEmpty()) {
delete toDelete;
}
}
global void finish(Database.BatchableContext bc) {
// Optional: send an email or log completion
System.debug('ArchiveOldAccountsBatch finished.');
}
}
Choosing a Batch Size
The Database.executeBatch method accepts a second parameter that sets the number of records processed per execute call. A safe default is 200–2000 records. Smaller batches reduce the chance of hitting the 5‑minute timeout, but increase the number of transactions and overall overhead. For most archiving jobs, a batch size of 500 works well.
Running and Monitoring the Batch
Run via Execute Anonymous
In a sandbox or developer edition, open the Developer Console or Execute Anonymous window and run:
Database.executeBatch(new ArchiveOldAccountsBatch(), 500);
Ensure you have Author Apex and Run Apex permissions.
Monitor via Apex Jobs Page
Navigate to Setup > Monitoring > Apex Jobs. Here you can see:
- Job status (Queued, Running, Completed, Failed)
- Number of batches processed
- Time taken per batch
- Error messages, if any
Click the View link for a batch to see the debug logs. Remember that logs for batch jobs are only kept for 24 hours unless you adjust the log retention settings.
Trade‑offs and Limitations
Added Complexity and Debugging
Batch Apex introduces asynchronous boundaries. A failure in one batch does not automatically roll back work done in previous batches, so you may end up with partial results. Always design idempotent logic or use Database.SaveResult to verify success.
Queue Limits
Salesforce allows up to 5 concurrent batch jobs by default. If you have many long‑running batches, they may queue behind each other, delaying processing. Monitor the Apex Jobs page and consider staggering starts or using Database.setBatchSize to shorten execution time.
Execution Timeouts
Each execute call has a 5‑minute limit. If your batch size is too large or your logic is inefficient, a batch can time out. Keep batch size modest and optimize your queries (e.g., use selective filters and indexed fields).
Actionable Checklist
- Define the target object and criteria (e.g., Accounts older than 5 years).
- Create a custom object (e.g.,
Archive__c) to hold archived data. - Implement a
Database.Batchableclass withstart,execute, andfinish. - Choose a batch size (start with 500).
- Test the batch in a sandbox with a small dataset (e.g., 10 records).
- Run the batch in the sandbox using Execute Anonymous.
- Verify records moved and check the Apex Jobs page for status.
- Deploy to production once confidence is established.
- Set up monitoring (e.g., a scheduled job that emails you on failure).
Next Steps
After mastering basic archiving, explore advanced patterns:
- Using
Database.emptyRecycleBinfor permanent deletion. - Chaining batches with
Database.executeBatchinfinishfor multi‑step workflows. - Implementing
Database.Statefulto maintain state across batches.
Batch Apex is a powerful tool for scaling data operations. By carefully designing your batch size, monitoring job status, and anticipating governor limits, you can safely move millions of records without compromising org performance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.