Fix Guides
How to Fix "Too Many SOQL Queries: 101" in Salesforce Apex
Step-by-step fix guide with AI-powered diagnosis from BuildForce.
Salesforce caps synchronous Apex at 100 SOQL queries per transaction (200 for async) — the exception fires on query #101. It's almost always a SOQL statement sitting inside a for-loop, or hiding inside a helper method that's called once per record instead of once per transaction. The fix: pull the query out of the loop, query the full set once with a WHERE ... IN clause, and index the results into a Map<Id, SObject> for lookups inside the loop.
Where This Usually Hides
- Direct SOQL in a for-loop — the obvious case, and the easiest to spot once you know to look.
- A "clean" helper or utility method that itself queries — called once per record from elsewhere, this is the most common way bulkified-looking code still fails.
- A getter invoked per row in a Visualforce controller or LWC Apex method backing a table.
- Trigger logic split across insert/update handlers that each independently query the same related data instead of sharing one bulkified pass.
- Recursive trigger re-entry that multiplies the same per-record query across multiple trigger invocations in one transaction.
How to Fix It — Step by Step
- 1. Get the exact class and line number. The stack trace in the error email or debug log names the class and line that threw. Start there — don't guess based on which trigger fired.
- 2. Search for hidden queries, not just obvious ones. Grep the failing class — and every method it calls — for '[SELECT'. The query causing the failure is often inside a small helper method that looks safe in isolation but is invoked once per record from a loop elsewhere.
- 3. Bulkify: one query, indexed into a map. Build a Set<Id> (or other filter values) from the full collection before the loop, run a single query with a WHERE ... IN clause, and put the results into a Map<Id, SObject> (or Map<Id, List<SObject>> for one-to-many) for O(1) lookups inside the loop.
- 4. Replace the per-record call with a map lookup. Swap the helper-method call (which queried per record) for a lookup into the map you built in the previous step. The loop body no longer touches the database at all.
- 5. Assert query counts in a bulk test. Call Limits.getQueries() before and after invoking the method with 200 records in a unit test, and assert the delta is a small constant — not proportional to record count. This turns a future regression into a failing test instead of a production incident.
- 6. Apply the same fix to Flow 'Get Records' elements. A Get Records element placed inside a Loop element hits the same underlying per-transaction query limit. Move it above the loop, query once into a collection variable, and reference the collection inside the loop instead.
- 7. Reach for async only when the work is genuinely too large. If a transaction legitimately needs more than 100 queries' worth of multi-object processing, move it to Queueable or Batch Apex (200-query ceiling). Don't use async as a substitute for fixing an unbulkified loop.
Example: The Hidden-Query Trap
// BAD — looks bulkified, but getRelatedCases() queries per record
for (Account acc : accountsToProcess) {
List<Case> cases = getRelatedCases(acc.Id); // <-- SOQL inside here
// ...
}
private List<Case> getRelatedCases(Id accountId) {
return [SELECT Id FROM Case WHERE AccountId = :accountId];
}
// GOOD — one query for the whole batch, indexed by AccountId
Set<Id> accountIds = new Map<Id, Account>(accountsToProcess).keySet();
Map<Id, List<Case>> casesByAccount = new Map<Id, List<Case>>();
for (Case c : [SELECT Id, AccountId FROM Case WHERE AccountId IN :accountIds]) {
if (!casesByAccount.containsKey(c.AccountId)) {
casesByAccount.put(c.AccountId, new List<Case>());
}
casesByAccount.get(c.AccountId).add(c);
}
for (Account acc : accountsToProcess) {
List<Case> cases = casesByAccount.get(acc.Id);
// ...
}This Error Is a Symptom, Not the Root Cause
A hidden query-in-a-loop is a code pattern, not a one-off mistake — if it shipped once, the same helper method or a copy of it is likely called from other unbulkified loops elsewhere in the org. Run BuildForce's free health check to see how your org's overall setup compares to similar companies, then connect your org so BuildForce can scan every Apex class for this pattern instead of waiting for the next bulk data load to find it for you.
How BuildForce Prevents This
- Continuous Apex health checks — surface SOQL-in-loop patterns, including queries hidden inside called methods, before they ship.
- Deployment automation — simulate governor limits against real org data volumes before code reaches production.
- AI consultant for Apex debugging — paste a stack trace and get a remediation plan in seconds.
FAQ
Is the limit 100 or 101 SOQL queries?
The governor limit is 100 SOQL queries per synchronous transaction (200 for asynchronous Apex — Batch, Queueable, and Future). The exception says 'Too many SOQL queries: 101' because the error fires on the query that pushes you past the 100 already issued — the 101st attempt is what throws.
Does Test.startTest() / Test.stopTest() reset the query limit?
Yes. Code inside Test.startTest()...Test.stopTest() gets a fresh set of governor limits, separate from setup code before it. This is exactly why a bulkification bug can pass a test that only inserts a handful of records — the limit reset masks it. Always test with at least 200 records inside startTest/stopTest to catch this class of bug before it reaches production.
Why do I get this error even though my trigger looks bulkified?
The trigger itself often is bulkified, but calls a 'clean-looking' helper or utility method once per record — and that method contains its own SOQL query. Grep the class (and everything it calls) for '[SELECT' and check every call site; a query that's perfectly safe called once becomes the bug the moment it's called inside someone else's loop.
Does moving the code to Batch Apex fix it?
It raises the ceiling to 200 queries per execute() invocation, but it doesn't fix unbulkified code — it just delays when you hit the wall, and it hides the real problem in a context that's harder to debug. Fix the bulkification first; move to async only for work that's genuinely too large for a single synchronous transaction.
Can BuildForce find these before they hit production?
Yes. BuildForce's Apex health checks scan for SOQL statements inside loops (directly or through a called method) and flag the specific class and line before the code ships, instead of waiting for it to fail against a bulk data load or a mass import in production.