Every Salesforce developer learns one rule early on: never put SOQL or DML inside a for loop. It’s drilled into new developers because it’s the fastest way to blow through governor limits. But here’s what many developers don’t fully understand: Salesforce provides a special kind of for loop, the SOQL for-loop, that is not only allowed but is actually the recommended pattern for processing large volumes of records safely.
In this guide, we’ll clear up the confusion around SOQL for-loops in Apex: what they are, why they exist, how they protect you from governor limits, and how to use them correctly to update thousands of records. By the end, you’ll understand the reasoning behind the rule, not just the rule itself which is what separates confident Salesforce developers from those who simply memorize best practices.
Why SOQL or DML Inside a Loop Is a Governor Limit Risk
Salesforce runs in a multi-tenant environment, so it enforces governor limits to prevent any single process from consuming too many shared resources. Two of the most important limits for Apex developers are:
- 100 SOQL queries per synchronous transaction
- 150 DML statements per transaction
Consider this classic anti-pattern:
// BAD: SOQL inside a loop
for (Account acc : accountList) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
// …
}
If accountList contains 200 records, this code fires 200 separate SOQL queries double the limit and throws a System.LimitException. The same problem occurs when you place an update or insert statement inside a loop: each iteration counts as its own DML statement.
This is why the rule exists. But the real rule isn’t “never use the word for near a query.” It’s “don’t fire a separate query or DML statement on every iteration.” That distinction is exactly where the SOQL for-loop comes in.
What Is the SOQL For-Loop in Apex?
The SOQL for-loop is a special Apex syntax that lets you write a query directly inside the for statement. It comes in two forms.
Form 1 one record at a time:
for (Account acc : [SELECT Id, Name FROM Account WHERE Industry = ‘Technology’]) {
// process one account at a time
}
Form 2 in batches of 200 (the more powerful option):
for (List<Account> accs : [SELECT Id, Name FROM Account WHERE Industry = ‘Technology’]) {
// ‘accs’ holds up to 200 records per iteration
}
The key is in how Salesforce handles this behind the scenes. The query itself counts as just one SOQL query, no matter how many records it returns. In Form 2, Salesforce automatically chunks the results into batches of 200 records per iteration.
Why the SOQL For-Loop Matters: Protecting the Heap Limit
Here’s the real reason the SOQL for-loop exists and it’s not primarily about the SOQL query limit. It’s about the heap size limit.
The heap is the memory your transaction uses to hold data while it runs. Synchronous transactions have a 6 MB heap limit (12 MB for asynchronous transactions). If you load a large list of records into memory all at once, like this:
// Loads ALL records into memory at once risky for large data volumes
List<Account> allAccounts = [SELECT Id, Name, Description FROM Account];
for (Account acc : allAccounts) {
// …
}
…and there are hundreds of thousands of records, you can exceed the heap limit and trigger a System.LimitException: Apex heap size too large.
The SOQL for-loop solves this elegantly. Because it processes records in batches of 200, only 200 records live in memory at any given moment, not the entire result set. Salesforce fetches the next batch as the loop continues, which is why the SOQL for-loop is the recommended way to iterate over large query results in Apex.
The Right Way to Update Thousands of Salesforce Records
So can you update 10,000 records using a for loop? Yes if you structure it correctly. The trick is to use the SOQL for-loop to iterate safely, collect records into a list, and run your DML on the batch (the list) rather than on each individual record.
// GOOD: SOQL for-loop + DML on the batch
for (List<Account> accBatch : [SELECT Id, Rating FROM Account WHERE Rating = null]) {
for (Account acc : accBatch) {
acc.Rating = ‘Warm’; // modify in memory
}
update accBatch; // ONE DML statement per batch of 200, not per record
}
Let’s count the governor limit usage for 5,000 records:
- SOQL queries: 1 (the query in the for-loop counts only once)
- Heap usage: safe only 200 records held in memory at a time
- DML statements: 5,000 ÷ 200 = 25 DML statements, comfortably under the 150-statement limit
Compare that to the naive approach of calling update acc; inside a loop over 5,000 records, which would attempt 5,000 individual DML statements and fail almost immediately. The end goal is the same, but the outcome is completely different because of how you structure the loop and the DML call.
Know the Limits of the SOQL For-Loop Pattern
The SOQL for-loop is powerful, but it isn’t a magic wand for unlimited data volumes. Keep these boundaries in mind:
- The 150 DML statement limit still applies. For 5,000 to 10,000 records, the batch approach stays comfortably under this limit. But if you tried to process 40,000 records this way, that would require 200 DML statements over the limit. For genuinely massive volumes, move to Batch Apex, which resets governor limits for each batch it processes.
- Total records retrieved by SOQL in a single transaction is capped at 50,000. The SOQL for-loop helps manage heap usage, but it doesn’t bypass this overall record limit.
- It’s best suited for read-and-process operations or moderate-volume updates. For very large-scale data operations, Batch Apex is the purpose-built tool.
A good rule of thumb: use the SOQL for-loop for iterating over large but reasonable result sets within a single transaction, and reach for Batch Apex when processing tens of thousands of records or more.
Quick Reference: Dos and Don’ts
Do:
- Use the SOQL for-loop (for (List<SObject> batch : [query])) to iterate over large result sets.
- Collect records into a list and perform DML on that list once per batch.
- Move to Batch Apex for very high-volume data operations.
Don’t:
- Place a standalone SOQL query inside a regular for loop.
- Place a DML statement (insert, update, or delete) inside a loop on a per-record basis.
- Assume the SOQL for-loop removes all governor limits; the 150 DML and 50,000 record limits still apply.
Conclusion
The lesson here isn’t just “use the SOQL for-loop.” It’s understanding why it works: it counts as a single SOQL query, and it protects your heap by processing records in batches of 200. Once you understand the reasoning behind Salesforce’s governor limits, patterns like this stop feeling like arbitrary rules and start feeling like obvious, sensible tools.
That’s the difference between memorizing Salesforce best practices and truly understanding the platform and it’s exactly the kind of understanding that builds confidence in daily development work and holds up under pressure in technical interviews.

