Salesforce Architecture

Salesforce Governor Limits Audit Checklist: 8 Bottlenecks

By Waleed Rafique, Salesforce Certified Developer · Published

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: 101 during imports, integrations or mass updates.
  • Cause: A query inside a for loop over Trigger.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 a Map. For the detailed refactor, see Common Causes of SOQL 101 Errors.

2. DML statements inside loops

  • Symptom: Too many DML statements: 151, or Too many DML rows: 10001 on large jobs.
  • Cause: insert or update called 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 exceeded on 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 Schema describe 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 for loops 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, or Too 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 LIMIT wherever 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.enqueueJob from 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:

  1. List your peak transactions. Integration upserts, data loads, month-end updates, lead conversion and any screen that saves related records.
  2. Reproduce them in a full or partial sandbox with realistic volumes. Running at least 200 records through every trigger path is the minimum.
  3. Capture debug logs with the Apex Profiling category at INFO or higher. Read the LIMIT_USAGE_FOR_NS section: 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.
  4. Instrument hot paths with the Limits class, 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()
    }));
  1. 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.
  2. Map the automation per object: triggers, Flows, Process Builders, Workflow Rules and managed packages.
  3. Check org-level limits through the REST /limits resource or OrgLimits.getMap(): daily API requests, async Apex executions and platform event allocations.
  4. 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.

FAQ

Frequently asked questions

Which governor limits fail most often?

In most orgs the CPU time limit, the 100 synchronous SOQL query limit and the 150 DML statement limit come up most often. These tend to come from logic that isn't bulkified, from recursion, or from several automations firing on one object. Heap and callout limits come up more in integration-heavy and batch code.

Can Flows hit governor limits?

Yes. Record-triggered and autolaunched Flows run in the same transaction as Apex and share the same limits. Get Records counts as a SOQL query, Create, Update and Delete Records elements count as DML, and Flow logic spends CPU time. Data elements placed inside loops are a common cause of limit errors.

How do I monitor Salesforce governor limits?

Use debug logs (the LIMIT_USAGE_FOR_NS section) in sandboxes, log Limits class values on high-volume code paths, and track org-wide allocations through the REST /limits resource or OrgLimits. Where licensed, Event Monitoring and Scale Center give deeper production visibility. Apex exception emails catch the failures that get past you.

Did the heap size limit change recently?

Yes. Winter '27 raised the Apex heap limit from 6 MB to 10 MB for synchronous transactions and from 12 MB to 25 MB for asynchronous ones. Callout request and response bodies are still limited to 6 MB synchronous and 12 MB asynchronous. Confirm your org's current value with Limits.getLimitHeapSize().

How often should we run a governor limits audit?

A sensible rhythm is before each major release, before large data migrations or integration launches, and after significant automation changes. Orgs where admins and developers add automation independently usually benefit from more frequent checks.

Get a second pair of eyes on your limits

If you'd rather have this checklist run against your org by someone who does it often, the 48-hour Salesforce Health & Limits Audit covers governor headroom, trigger recursion, Flow conflicts and test coverage for a fixed €850. You get an executive report and a prioritised remediation backlog.