Skip to content

System.LimitException: Too many SOQL queries: 101

The limit is per transaction, not per class, so bulkified code still fails when it is fifth in a trigger chain. How to find the real cause and fix it.

TL;DR

  • Apex allows 100 SOQL queries per synchronous transaction and 200 in asynchronous contexts such as Batch, Queueable and @future. It is a hard limit and Salesforce support cannot raise it.
  • The error says 101 because it fires on the query that exceeded the ceiling, so the query in the stack trace is rarely the one at fault. It is just the one that arrived last.
  • The classic cause is a SOQL query inside a loop. The classic fix is to collect ids into a Set and run one query outside the loop using IN.
  • The cause that catches experienced teams is different: the limit counts the entire transaction, not your class. Well-written code fails because it is fifth in a trigger chain that has already spent 97 queries.
  • Moving work to @future raises the ceiling to 200 but does not fix the design, and it can make things worse by multiplying transactions.
  • Do not guess. Limits.getQueries() and Limits.getLimitQueries() tell you exactly where you are at any point in the transaction.

What You'll Learn

  • What actually counts toward the limit, and what does not
  • Why the failing query in your stack trace is usually innocent
  • The bulkification pattern, and how to apply it to a trigger you did not write
  • How to diagnose a transaction that fails only in production
  • When asynchronous execution genuinely helps, and when it hides the problem

The Problem

The error arrives with a stack trace pointing at a line of code, and the natural response is to look at that line. Usually there is nothing wrong with it.

Too many SOQL queries: 101 means the transaction asked for its 101st query. The one that tipped it over gets the blame, but the transaction may have spent its budget across a dozen classes, several triggers, a flow, and a managed package, before ever reaching your line.

That is why this error has a reputation for appearing suddenly in code nobody touched. Somebody added a trigger, or activated a flow, or installed a package, and your query went from being number 40 to being number 101.

Common questions this article answers:

  • What is the actual limit, and can it be raised?
  • Why does the error point at code that looks fine?
  • How do I fix it when the queries are not in code I own?

Quick Answer

Apex allows 100 SOQL queries in a synchronous transaction and 200 in an asynchronous one, including queries issued by triggers, classes, flows and managed packages in the same transaction. It is a hard limit. The most common cause is a query inside a for loop, which turns 200 records into 200 queries, and the fix is to collect the ids you need into a Set and run a single query outside the loop with an IN clause, storing the results in a Map keyed by id. Because the limit is counted per transaction rather than per class, bulkified code can still fail when it runs late in a long chain of automation, so diagnose with Limits.getQueries() rather than reading the stack trace. Query the same object once per transaction and pass the results around instead of re-querying. Asynchronous execution doubles the ceiling and is a legitimate tool for genuinely deferrable work, but using it to escape a design problem usually relocates the failure rather than removing it.

What counts, and what does not

Worth being precise, because the mental model determines where you look.

Counts toward the 100: every SOQL statement executed in the transaction, wherever it comes from. Your Apex, other people's Apex, triggers on objects your DML touched, flows, process automation, and managed package code that runs in the same transaction.

Does not count: SOSL searches, which have their own limit. Rows returned, which are governed separately by the 50,000 row limit. Queries in a genuinely separate transaction, such as one started by @future or a Queueable.

Two implications follow, and both are counterintuitive.

A query in a loop over 200 records is 200 queries, so a single badly written loop consumes the entire budget twice over. This is why the error tends to appear as soon as someone runs a bulk update rather than saving one record in the UI.

Your code is not alone in the transaction. A DML statement on Account fires Account triggers, which may fire flows, which may update Contacts, which fire Contact triggers. Every query along that chain draws on the same 100.

The query in the stack trace is innocent

This is the single most useful thing to internalise, because it changes where you spend your time.

When the transaction hits 101, execution stops at whichever query happened to be next. If your class is the last thing to run, your query gets named, even if it accounts for exactly one of the 101.

So resist the instinct to optimise the line in the trace. Instead, find out where the budget went:

System.debug('SOQL used: ' + Limits.getQueries() + ' of ' + Limits.getLimitQueries());

Drop that at the start and end of the suspect entry point. If your code starts at 97, the problem is upstream and no amount of rewriting your class will fix it. If your code starts at 3 and ends at 101, it is yours.

The same numbers appear in a debug log's cumulative limit usage section, which is usually faster than adding instrumentation when the failure is reproducible.

The bulkification pattern

The fix for the common case is mechanical. The shape to recognise:

// Fails at 101 the moment this handles more than 100 records
for (Opportunity opp : Trigger.new) {
    Account acct = [SELECT Id, Industry FROM Account WHERE Id = :opp.AccountId];
    opp.Industry__c = acct.Industry;
}

One query per iteration. Two hundred records, two hundred queries.

The corrected shape queries once and looks up in memory:

// One query regardless of volume
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : Trigger.new) {
    if (opp.AccountId != null) accountIds.add(opp.AccountId);
}

Map<Id, Account> accountsById = new Map<Id, Account>(
    [SELECT Id, Industry FROM Account WHERE Id IN :accountIds]
);

for (Opportunity opp : Trigger.new) {
    Account acct = accountsById.get(opp.AccountId);
    if (acct != null) opp.Industry__c = acct.Industry;
}

Three things are doing the work. The Set deduplicates, so a hundred opportunities on one account produce one id. The IN clause turns many lookups into one query. The Map constructor gives you id-keyed access without a second pass.

The null check on AccountId matters more than it looks. Without it you add null to the set, which is a wasted position in the query and a source of confusing results.

When the queries are not yours

The harder case, and the more common one in a mature org.

If instrumentation shows the budget is gone before your code runs, you have an architectural problem rather than a coding one, and the options are ordered by how much you control:

Consolidate queries within your own domain first. If three of your classes each query Account in the same transaction, that is three of a shared 100 for one object. Query once at the entry point and pass the records down. A simple request-scoped cache, a static map populated on first use, removes repeat queries without restructuring anything.

Find out what else is running. A debug log shows the full execution tree. Look for triggers firing on objects you update as a side effect, flows on the same object, and validation or rollup logic that queries. Teams are routinely surprised by how many separate automations touch one save.

Reduce the number of things that run at all. Multiple triggers per object is the usual culprit, and consolidating to a single trigger with a handler class is the standard remedy, mainly because it makes the execution order visible and controllable rather than emergent.

Then consider asynchronous. Not first. Moving work to a Queueable gives it a fresh transaction with a 200 ceiling, which genuinely helps when the work does not need to complete before the user's save returns. It does not help if the work must be synchronous, and it introduces new failure modes: async jobs can fail silently, they have their own queue limits, and a @future called from a loop creates its own kind of mess.

Frequently Asked Questions

Q: Can Salesforce raise the limit for us?

A: No. This is a hard governor limit, not a configurable allocation, and support cannot increase it. The limit exists because Apex runs on shared infrastructure, so the answer is always to reduce query count rather than request headroom.

Q: Why does this only happen in production?

A: Usually volume and automation. A sandbox with ten records never exercises a query in a loop hard enough to fail, and sandboxes often have fewer active flows, packages and integrations contributing queries to the same transaction. Test with a realistic bulk operation of 200 records rather than saving one record in the UI.

Q: Does a query that returns no rows still count?

A: Yes. The limit counts queries issued, not rows returned or results found. A query inside a loop that finds nothing still consumes one of your 100 each time round.

Q: Should I just move everything to @future?

A: No. Asynchronous contexts get 200 rather than 100, which is a larger bucket rather than a different design, and the underlying inefficiency travels with the code. Use async when work is genuinely deferrable, not to escape a limit, or you will meet the same error later in a place that is harder to debug.

Q: How do I find which automation is spending the budget?

A: Reproduce it with a debug log at FINEST for Apex and Workflow, then read the cumulative resource usage section, which reports SOQL used against the limit. The execution tree above it shows every trigger, flow and class that ran, which is normally where the surprise is.

Key Takeaways

  • 100 queries synchronous, 200 asynchronous, counted across the whole transaction including flows and managed packages.
  • The query in the stack trace is usually innocent. It is the one that arrived at a full bucket.
  • Collect ids into a Set, query once with IN, look up through a Map. That is the whole pattern for the common case.
  • Instrument rather than guess. Limits.getQueries() tells you whether the problem is yours or upstream.
  • Query each object once per transaction and pass results around, rather than re-querying in each class.
  • Asynchronous is a bigger bucket, not a better design. Use it for deferrable work, not to escape a limit.

What's Next?

Recommended Reading:

Action Items:

  1. Add Limits.getQueries() logging at the entry and exit of the failing operation to find out whether the budget is yours.
  2. Search your codebase for [SELECT inside for ( blocks, which finds most instances mechanically.
  3. Count how many triggers, flows and packages run on the objects involved, since that is usually where a mature org's budget goes.

Resources & References

Responses

Checking your session.

Loading responses.