Diagnosing and Fixing Apex Batch "Too Many SOQL Queries" Errors
Apex batch jobs often hit the 100‑SOQL‑query limit. This guide shows how to spot the symptom, identify the root cause with a concise table, run ordered checks, apply targeted fixes, and know when to raise the issue to a senior developer.
19 Sept 2025, 12:28 UTC

Recognizable Condition
When a batch job fails with the runtime error Too many SOQL queries: 101, the transaction has executed more than the allowed 100 SOQL queries. In the debug log you will see the line:
System.LimitException: Too many SOQL queries: 101
Typical symptoms: the batch stops after a few executions, the batch apex class throws an exception, and the batch status in the Apex Jobs page shows Failed.
Common Causes
The following patterns are the most frequent culprits:
- SOQL inside a
forloop inexecute()– one query per record. - Large
batchSize(e.g., 2000) – many records per transaction, so each query is multiplied. - Trigger‑side SOQL that is not bulkified – each trigger invocation adds a query.
- Recursive trigger execution on the same object during batch processing.
- Missing use of
@futureorQueueableto offload heavy queries.
Diagnostic Table
| Finding | Likely Cause | Check |
|---|---|---|
| Query count spikes after first 200 records | Query inside loop | Search for SOQL inside for loops in execute() |
Batch fails only when batchSize > 500 | Large batch size amplifying queries | Review Database.executeBatch call |
| Trigger logs show >100 queries per transaction | Unbulkified trigger | Examine trigger code for per‑record queries |
Stack trace shows repeated MyObjectTrigger calls | Recursive trigger | Check for self‑inserting/updating within trigger |
| Batch runs but data is incomplete | Missing async offload | Verify expensive queries are moved to @future or Queueable |
Ordered Checks
- Inspect the Batch Class
Open the batch class (e.g.,MyBatchJob) and locate theexecute()method. Search for anySELECTstatements that appear inside aforloop that iterates overscopeor any collection. - Check
batchSize
Look at theDatabase.executeBatchcall. If the size is >500, consider reducing it to 200 or 100 for testing. Note that a smaller size will increase the number of executions but keep each transaction within limits. - Audit Triggers
Open any triggers that fire on the objects processed by the batch. Look for SOQL inbeforeorafterblocks that run per record. Use the “Bulk‑Friendly” pattern: collect IDs first, then run a single query. - Detect Recursion
In the trigger, search for DML on the same object it is defined on. If found, add a static flag to prevent re‑entry. - Review Async Offload
Check if the batch calls any@futureorQueueableclasses that perform SOQL. Ensure those calls are batched themselves or useDatabase.executeBatchwith a smaller size. - Examine Debug Logs
Run the batch withDebug Level: Apex Code, Apex Profiling. Look for theSOQL: SELECTlines and count them per transaction. The log will showSOQL: SELECT…followed bySOQL: SELECT…for each query.
Fixes Tied to Findings
- Move Queries Out of Loops
Replace per‑record queries with a single query that collects all needed data. Example:// Bad for (Account a : scope) { List con = [SELECT Id FROM Contact WHERE AccountId = :a.Id]; // ... } // Good Set acctIds = new Set(); for (Account a : scope) acctIds.add(a.Id); Map> conMap = new Map>(); for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :acctIds]) { conMap.putIfAbsent(c.AccountId, new List()); conMap.get(c.AccountId).add(c); } // ... - Reduce
batchSize
If you cannot bulkify immediately, temporarily setbatchSizeto 200 and rerun. Verify the query count remains below 100. - Bulkify Triggers
Apply the same pattern as above inside the trigger. UseDatabase.queryonly once per transaction. - Add Static Flag for Recursion
Add a static Boolean flag in the trigger class:public class MyTriggerHandler { public static Boolean isRecursive = false; } // In trigger if (!MyTriggerHandler.isRecursive) { MyTriggerHandler.isRecursive = true; // DML logic MyTriggerHandler.isRecursive = false; } - Offload Heavy Queries
Move expensive queries to aQueueableclass and enqueue it from the batch. Example:public class HeavyQueryJob implements Queueable { public void execute(QueueableContext ctx) { // Complex SOQL } } // In batch Database.executeBatch(new HeavyQueryJob(), 1);
Escalation Criteria
- After applying all fixes, the batch still fails with
Too many SOQL queries. - Debug logs show more than 100 SOQL queries even after reducing
batchSizeto 200. - There are more than 5 concurrent batch jobs running; the governor limit is hit across jobs.
- The batch performs data‑critical operations and cannot be split further.
Practical Verification Example
Assume you have a batch that updates Opportunity records based on related Account data. The original class:
global class OpportunityUpdateBatch implements Database.Batchable {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, AccountId FROM Opportunity');
}
global void execute(Database.BatchableContext bc, List scope) {
List opps = (List)scope;
for (Opportunity o : opps) {
Account a = [SELECT Industry FROM Account WHERE Id = :o.AccountId LIMIT 1];
o.Custom_Field__c = a.Industry;
}
update opps;
}
global void finish(Database.BatchableContext bc) {}
}
Diagnostic steps:
- Run the batch with
batchSize = 2000→ fails after first execution. - Inspect
execute()→ SOQL inside loop. - Rewrite as bulkified query (shown above). Re‑run with
batchSize = 2000→ success.
Verify by checking the debug log: you should see only 1 SELECT per transaction, not one per Opportunity.
Summary
When an Apex batch hits the SOQL query limit, the root cause is almost always a per‑record query or an unbulkified trigger. Use the diagnostic table to pinpoint the issue, apply the corresponding fix, and validate with debug logs. If the problem persists after all fixes, bring the case to a senior developer or Salesforce support, noting the concurrency and data‑critical nature of the job.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.