Briefly: you have a SOQL query inside a loop. Salesforce allows 100 SOQL queries per synchronous transaction, and the exception fires when your code attempts the 101st. Move the query outside the loop, collect what you need in one go, and look results up from a Map. Everything below is about finding which loop, because in a real org it is rarely the one you expect.
What the number actually means
The limit is per transaction, not per class or per trigger. Every query issued by every piece of code that runs inside a single save counts toward the same total, including automation you did not write.
The documented per-transaction limits are:
| Limit | Synchronous | Asynchronous |
|---|---|---|
| SOQL queries issued | 100 | 200 |
| Records retrieved by SOQL | 50,000 | 50,000 |
| DML statements | 150 | 150 |
| Records processed by DML | 10,000 | 10,000 |
| CPU time | 10,000 ms | 60,000 ms |
Two consequences follow from “per transaction” that catch people out. First, the same code can pass in a unit test and fail in production, because production has more automation stacked on the object. Second, running asynchronously doubles your query budget to 200, which is why a batch job sometimes succeeds where a trigger fails on identical logic. Asynchronous headroom is a reprieve, not a fix.
Cause 1: a query inside a for loop
This accounts for most occurrences. It often does not look like a loop containing a query, because the query lives one method call away.
// Fails once 100 opportunities are processed together.
for (Opportunity opp : Trigger.new) {
List<OpportunityLineItem> items = [
SELECT Id, Quantity FROM OpportunityLineItem
WHERE OpportunityId = :opp.Id
];
// ...
}
Query once for every parent, then group the results:
Map<Id, List<OpportunityLineItem>> itemsByOpp = new Map<Id, List<OpportunityLineItem>>();
for (OpportunityLineItem item : [
SELECT Id, Quantity, OpportunityId FROM OpportunityLineItem
WHERE OpportunityId IN :Trigger.newMap.keySet()
]) {
if (!itemsByOpp.containsKey(item.OpportunityId)) {
itemsByOpp.put(item.OpportunityId, new List<OpportunityLineItem>());
}
itemsByOpp.get(item.OpportunityId).add(item);
}
for (Opportunity opp : Trigger.new) {
List<OpportunityLineItem> items = itemsByOpp.get(opp.Id);
if (items == null) { continue; }
// ...
}
One query regardless of batch size. Note the null check on the Map lookup: parents with no children are absent from the Map, not present with an empty list, and skipping that check trades your governor exception for a null pointer exception.
Cause 2: the query is hidden behind a helper method
The loop and the query are in different files, so neither looks wrong on its own.
for (Case c : Trigger.new) {
if (AccountService.isStrategic(c.AccountId)) { // queries inside
// ...
}
}
Utility methods that accept a single Id are the usual culprit. Any method that takes one record and returns data from the database will become a governor problem the first time somebody calls it in a loop. Give those methods a bulk signature — accept a Set<Id> and return a Map, and the misuse becomes impossible rather than merely discouraged.
Cause 3: recursion
Your trigger updates a record, that update fires the trigger again, and each pass spends more of the same query budget. The count is cumulative across the whole transaction, so three passes of 35 queries will fail even though no single pass looks unreasonable.
A static guard is the conventional fix:
public class AccountTriggerHandler {
private static Boolean hasRun = false;
public static void handleAfterUpdate(List<Account> accounts) {
if (hasRun) { return; }
hasRun = true;
// ...
}
}
Be deliberate about this one. A static flag suppresses the second pass entirely, which is correct when the second pass is redundant work and wrong when it carries changes that genuinely need processing. If the recursion comes from a same-record field update, the better fix is to stop causing the second save at all: do the update in a before-save context instead.
Cause 4: nothing you wrote
The transaction includes every managed package trigger, every flow, and every other automation on the object. Your code can be perfectly bulkified and still be the one that trips the limit because it happened to run last, holding the unlucky 101st query.
Check the debug log rather than guessing. Set the Apex Code log level to FINEST and look for the LIMIT_USAGE_FOR_NS entries at the end. Those are broken out per namespace, so you can see how much of the budget your code consumed versus each installed package. If your namespace accounts for eleven queries out of a hundred, the fix is not in your code, and the honest options are to move your work asynchronously or to reduce how much automation runs on that object.
Confirming the fix
Instrument the count rather than trusting that it improved:
System.debug('Queries used: ' + Limits.getQueries() + ' of ' + Limits.getLimitQueries());
Then prove it at volume. A test that inserts one record proves nothing about a governor limit — the only test that matters inserts 200:
@IsTest
static void bulkInsertStaysWithinQueryLimit() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 200; i++) {
accounts.add(new Account(Name = 'Bulk Test ' + i));
}
Test.startTest();
insert accounts;
System.assert(Limits.getQueries() < 100, 'Query count grows with batch size');
Test.stopTest();
}
If that assertion passes at 200 records, your query count no longer scales with the batch, which is the actual property you were trying to achieve.
Stopping it happening again
- Query at the top, loop at the bottom. Collect everything the transaction needs first, then iterate over in-memory collections.
- Bulk signatures only. No shared method should accept a single Id and query.
- One trigger per object routing into a handler, so ordering and re-entry are yours to control instead of emergent.
- Test at 200 for anything that touches the database.
- Prefer before-save updates for same-record field changes, which avoids the second save cycle entirely.
Frequently asked
Why 101 and not 100?
The limit is 100. The exception names the query that exceeded it, so 101 is the first one that could not run.
Does running asynchronously fix it?
It raises the ceiling to 200, which buys headroom rather than fixing the underlying scaling problem. Code that issues one query per record will fail again at a larger batch size.
Do queries inside a called flow count toward my Apex limit?
Yes. The limits are per transaction and span everything that runs in that transaction, Apex and declarative automation alike.
Does SOQL For Update or a query with LIMIT 1 count?
Every query counts as one, regardless of clauses or how many rows come back. A hundred single-row queries cost exactly as much of this limit as a hundred full-table queries.
Can I raise the limit?
No. Per-transaction governor limits are not adjustable by support or by licence.
Reference: Per-Transaction Apex Governor Limits, Salesforce Apex Developer Guide.