To find what's about to hit a Salesforce governor limit, measure headroom rather than waiting for failures. Run your busiest transactions with realistic data volumes, read the cumulative limit usage in debug logs, and check the code for the eight usual bottlenecks: queries and DML in loops, recursion, overlapping automation, heavy CPU work, heap and callouts.
What governor limits are and why they fail at peak load
Salesforce is multitenant, so the Apex runtime enforces per-transaction limits to stop any one org's code from monopolising shared resources. When code exceeds one, Salesforce throws a System.LimitException that can't be caught, and the whole transaction rolls back.
The per-transaction limits most teams run into are listed in Salesforce's Execution Governors and Limits documentation:
| Limit | Synchronous | Asynchronous |
|---|---|---|
| SOQL queries issued | 100 | 200 |
| Records retrieved by SOQL | 50,000 | 50,000 |
| DML statements issued | 150 | 150 |
| Records processed by DML | 10,000 | 10,000 |
| CPU time on Salesforce servers | 10,000 ms | 60,000 ms |
| Heap size | 10 MB (6 MB before Winter '27) | 25 MB (12 MB before Winter '27) |
| Callouts per transaction | 100 | 100 |
| Trigger recursion stack depth | 16 | 16 |
A note on heap: Winter '27 raised the heap limits from 6 MB/12 MB to 10 MB/25 MB. If any of your orgs are still on Summer '26, plan against the old figures, and check the live value with Limits.getLimitHeapSize(). Don't assume it.
Limits rarely fail in a sandbox. They fail at peak load because that's when transactions get big. A data load, an integration sync or a mass update sends records through triggers in chunks of 200, and every chunk of the same DML statement shares one transaction's limits. A trigger that runs three queries per record gets away with it when a rep saves one Opportunity. It doesn't get away with it when the ERP sync pushes 200 at once. Peak periods also stack automation: month-end updates set off Flows, which update child records, which run more triggers.
The 8 bottlenecks: symptom, cause and fix
1. SOQL queries inside loops
- Symptom:
System.LimitException: Too many SOQL queries: 101during imports, integrations or mass updates. - Cause: A query inside a
forloop overTrigger.new, or a helper method that queries and gets called once per record. - Fix: Collect IDs first, run one query with
WHERE Id IN :ids, and look records up in aMap. For the detailed refactor, see Common Causes of SOQL 101 Errors.
2. DML statements inside loops
- Symptom:
Too many DML statements: 151, orToo many DML rows: 10001on large jobs. - Cause:
insertorupdatecalled once per record, or child records saved one parent at a time. - Fix: Add records to a list and run one DML statement per object type. For jobs that touch more than 10,000 rows, move the work to Batch Apex or a Queueable chain.
3. Trigger recursion
- Symptom:
Maximum trigger depth exceeded, duplicate child records, or CPU errors that only show up on updates. - Cause: A trigger updates its own object, or object A updates B, whose trigger updates A. A static Boolean "run once" flag hides the problem but skips later chunks of 200 in the same transaction.
- Fix: Use a trigger handler framework with one trigger per object. Guard recursion with a static
Set<Id>of records already processed rather than a Boolean:
public with sharing class OpportunityTriggerHandler {
private static Set<Id> processedIds = new Set<Id>();
public static void afterUpdate(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
List<Opportunity> toProcess = new List<Opportunity>();
for (Opportunity opp : newList) {
if (!processedIds.contains(opp.Id)
&& opp.StageName != oldMap.get(opp.Id).StageName) {
toProcess.add(opp);
processedIds.add(opp.Id);
}
}
if (!toProcess.isEmpty()) {
OpportunityService.syncStageChanges(toProcess);
}
}
}
4. Overlapping automation on the same object
- Symptom:
Apex CPU time limit exceededon saves that look simple, often on Account, Opportunity or Case. - Cause: Several Apex triggers, record-triggered Flows and leftover Process Builders or Workflow Rules all firing on one save, sometimes updating the same fields. They all spend the same CPU budget.
- Fix: List every automation per object and per event. Move same-record field updates to before-save Flows or before triggers, retire Process Builder and Workflow Rules, and give each object a clear order of execution.
5. CPU-heavy Apex logic
- Symptom: CPU time errors with plenty of SOQL and DML headroom left.
- Cause: Nested loops over large collections, string concatenation in loops, repeated
Schemadescribe calls, or serialising large JSON. CPU time also includes managed package code and workflows called from your transaction. - Fix: Replace nested loops with map lookups, cache describe results in static variables, and move non-urgent work to Queueable Apex, which gets the 60-second asynchronous limit.
6. Heap size pressure
- Symptom:
Apex heap size too large, usually in batch jobs, file handling or callout parsing. - Cause: Holding whole query results, Long Text or Blob fields, or large deserialised payloads in memory. HTTP request and response bodies count towards heap too.
- Fix: Query only the fields you need, use SOQL
forloops so records are processed in chunks of 200, clear collections once you've finished with them, and reduce the Batch Apex scope size. The Winter '27 increase gives you more room, but it doesn't excuse unbounded collections.
7. Non-selective SOQL and row limits
- Symptom:
Non-selective query against large object type, slow saves, orToo many query rows: 50001. - Cause: Trigger queries filtering on unindexed fields, negative operators or leading wildcards against objects with more than 200,000 records.
- Fix: Filter on indexed fields (Id, Name, external IDs, lookups, or custom indexes), check queries with the Query Plan tool, and add
LIMITwherever the business logic allows.
8. Synchronous callouts and async job limits
- Symptom:
You have uncommitted work pending, timeouts,Too many queueable jobs added to the queue, or saves blocked during a partner outage. - Cause: Callouts made directly from save logic, more than one
System.enqueueJobfrom an async context (the limit is 1, against 50 synchronously), or long-running requests piling up against the org's concurrency limit for transactions longer than five seconds. - Fix: Separate callouts from the save: publish a Platform Event or enqueue a single Queueable per transaction, then make the callout asynchronously with retries.
How to run the audit
Go in this order, so the measurement tells you where to read the code first:
- List your peak transactions. Integration upserts, data loads, month-end updates, lead conversion and any screen that saves related records.
- Reproduce them in a full or partial sandbox with realistic volumes. Running at least 200 records through every trigger path is the minimum.
- Capture debug logs with the Apex Profiling category at
INFOor higher. Read theLIMIT_USAGE_FOR_NSsection: it reports cumulative usage, such as "Number of SOQL queries: 87 out of 100". Anything above roughly 50% of a limit on a normal-sized transaction deserves attention. - Instrument hot paths with the
Limitsclass, so integration logs record headroom on every run:
System.debug(LoggingLevel.INFO, String.format(
'SOQL {0}/{1} | DML {2}/{3} | CPU {4}/{5} ms | Heap {6}/{7}',
new List<Object>{
Limits.getQueries(), Limits.getLimitQueries(),
Limits.getDmlStatements(), Limits.getLimitDmlStatements(),
Limits.getCpuTime(), Limits.getLimitCpuTime(),
Limits.getHeapSize(), Limits.getLimitHeapSize()
}));
- Run a static scan. Salesforce Code Analyzer (PMD rules) flags SOQL and DML in loops, and ApexGuru can surface performance antipatterns where your edition includes it.
- Map the automation per object: triggers, Flows, Process Builders, Workflow Rules and managed packages.
- Check org-level limits through the REST
/limitsresource orOrgLimits.getMap(): daily API requests, async Apex executions and platform event allocations. - Rank the findings by blast radius (which business process fails) and by headroom left, then turn them into tickets.
When to bring in help
You can run this checklist yourself. An outside review starts to make sense when limit errors are already reaching users, nobody on the team fully understands the trigger and Flow chains, a big data migration or integration launch is coming up, or deployments keep stalling on brittle tests. Someone reading the org for the first time often spots coupling that the team stopped seeing long ago. If the problem keeps coming back because nobody reviews new automation before it ships, ongoing oversight, such as a fractional Salesforce architect reviewing pull requests and Flow changes, tends to help more than a one-off fix.