Governor Limits Reference

Salesforce Governor Limits —
Complete Reference & Best Practices

Every governor limit that matters for Salesforce developers: SOQL, DML, heap size, CPU time, callouts, email, batch, and queueable — with practical code patterns to stay well within them.

🕐 ~16 min read 💻 Apex & SOQL 👥 All experience levels
Advertisement

1 What Are Governor Limits and Why Do They Exist?

Salesforce runs as a multi-tenant platform — your organisation shares the same servers, database, and runtime infrastructure with thousands of other organisations. To ensure fair resource usage and protect platform stability, Salesforce enforces governor limits: hard caps on how many resources a single Apex transaction can consume.

If your code exceeds any governor limit, the platform immediately throws a System.LimitException (an uncatchable exception) and rolls back the entire transaction. There is no warning before the exception — the transaction is simply terminated. This makes understanding and respecting governor limits non-negotiable for any Salesforce developer.

What counts as a "transaction"?

A transaction is a single unit of execution that shares a limit pool. Examples of what constitutes one transaction include:

  • A trigger firing on an insert of 200 records
  • An anonymous Apex script executed via the Developer Console
  • A single invocation of a future method
  • A single batch execute() method call
  • A single queueable job execution
  • A Visualforce page request or a REST API call into an @RestResource class
ℹ️
Key insight: Governor limits are scoped per transaction, not per class or per method. Every SOQL query issued anywhere in the call stack — triggers, helper classes, utility methods, all of it — counts toward the same pool of 100 (sync) or 200 (async) queries.

Synchronous vs. asynchronous limits

Many governor limits have two tiers: a lower limit for synchronous execution (triggers, Visualforce controllers, REST endpoints) and a higher limit for asynchronous execution (future methods, queueable apex, batch apex, scheduled apex). Async contexts are given more headroom precisely because they run in the background and do not block the user.

⚠️
Common misconception: Many developers assume that calling a helper class "bulkifies" their code. It does not — if that helper class runs a SOQL query, it still consumes from the transaction's shared query pool. Bulkification is a design pattern, not a class-level concept.

2 Complete Limits Reference Table

The table below covers every frequently referenced governor limit, split into synchronous and asynchronous values. Memorise the synchronous column — it is what you hit in triggers, Visualforce, and REST controllers every day.

100
SOQL queries (sync)
50,000
SOQL rows returned
150
DML statements (sync)
10,000
DML rows processed
6 MB
Heap size (sync)
10 sec
CPU time (sync)
Resource Synchronous Limit Asynchronous Limit Category
SOQL queries per transaction 100 200 Query
Records returned by SOQL 50,000 50,000 Query
Records retrieved via Database.getQueryLocator 50,000,000 Query
SOSL searches per transaction 20 20 Query
Records returned by a single SOSL query 2,000 2,000 Query
DML statements per transaction 150 150 DML
DML rows processed per transaction 10,000 10,000 DML
Heap size 6 MB 12 MB Memory
CPU time 10,000 ms 60,000 ms CPU
Maximum execution time per transaction 10 min 10 min CPU
HTTP callouts per transaction 100 100 Callout
Maximum callout timeout 120 sec 120 sec Callout
Email invocations per transaction 10 10 Email
Single email messages per day (org-wide) 1,000 per licence (max 1,000,000) Email
@future method calls per transaction 50 0 (cannot enqueue from async) Async
Queueable jobs added per transaction 50 1 (chain limit) Async
Batch Apex jobs in the Apex flex queue 100 Async
Concurrent batch jobs in ACTIVE/PROCESSING state 5 Async
Scheduled Apex jobs 100 Async
Push notification messages per transaction 10 10 Misc
Describe sObject calls per transaction 100 200 Misc
🛑
LimitException is uncatchable. Unlike other Apex exceptions, System.LimitException cannot be caught with a try/catch block. Once you exceed a governor limit, the transaction is dead — your only options are to prevent the violation in the first place through good design, or to split work across async boundaries.

3 SOQL in Loops — The #1 Anti-Pattern

The most commonly violated governor limit in Salesforce development is the SOQL query limit, and it is almost always caused by a single mistake: issuing a SOQL query inside a for loop. Because a trigger can process up to 200 records in one invocation, a query inside the loop fires once per record — instantly consuming 200 of your 100-query budget and throwing a LimitException.

Anti-pattern: SOQL inside a loop

Apex Trigger — BAD Pattern DO NOT USE
// WRONG — this fires one SOQL query per record in Trigger.new
// With 200 records, this immediately blows the 100-query limit
trigger OpportunityTrigger on Opportunity (after update) {
    for (Opportunity opp : Trigger.new) {
        // BAD: one query per iteration
        Account acc = [
            SELECT Id, Name, Industry
            FROM Account
            WHERE Id = :opp.AccountId
        ];
        // ... do something with acc
    }
}

Correct pattern: bulk SOQL before the loop, Map for lookup

Apex Trigger — GOOD Pattern RECOMMENDED
// CORRECT — one SOQL query regardless of how many records trigger fires on
trigger OpportunityTrigger on Opportunity (after update) {

    // Step 1: collect all AccountIds from the trigger batch in one pass
    Set<Id> accountIds = new Set<Id>();
    for (Opportunity opp : Trigger.new) {
        if (opp.AccountId != null) {
            accountIds.add(opp.AccountId);
        }
    }

    // Step 2: one bulk SOQL — returns ALL matching accounts
    Map<Id, Account> accountMap = new Map<Id, Account>(
        [SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds]
    );

    // Step 3: iterate with O(1) Map lookup — zero additional SOQL
    for (Opportunity opp : Trigger.new) {
        Account acc = accountMap.get(opp.AccountId);
        if (acc != null) {
            // ... do something with acc
        }
    }
}

Why Map<Id, SObject> is the canonical solution

The new Map<Id, Account>(queryResult) constructor is a Salesforce idiom that automatically builds a Map keyed by the record's Id field. This gives you O(1) lookup time inside your loop, uses exactly one SOQL query, and handles any number of records in the trigger batch — from 1 to 200.

The golden rule: Never put a SOQL query inside a for loop. Collect all the IDs you need before the loop, run one bulk query with WHERE Id IN :idSet, store results in a Map, and look up inside the loop.

SOQL for loops — iterating over query results efficiently

When you need to process a large result set that approaches the 50,000 row limit, use the SOQL for loop syntax. It processes records in chunks of 200 without loading all results into heap at once, protecting you from both the 50,000 row limit and the 6 MB heap size limit simultaneously.

Apex — SOQL For Loop (chunked processing) EFFICIENT
// SOQL for loop: processes in batches of 200 internally
// Avoids loading all rows into heap at once
for (List<Contact> batch : [
    SELECT Id, Email, AccountId
    FROM Contact
    WHERE Email != null
]) {
    // 'batch' is a List of up to 200 Contacts
    // process each chunk here, then GC can reclaim the memory
    for (Contact c : batch) {
        // process individual record
    }
}

4 DML in Loops — The #2 Anti-Pattern

The second most common governor limit violation is issuing DML statements inside a loop. The synchronous DML statement limit is 150 per transaction. If you perform an update, insert, or delete inside a loop that iterates over trigger records, you consume one DML statement per iteration and quickly exhaust the limit.

Anti-pattern: DML inside a loop

Apex — BAD DML Pattern DO NOT USE
// WRONG — one DML statement per record; burns the 150-statement limit fast
trigger ContactTrigger on Contact (after insert) {
    for (Contact c : Trigger.new) {
        Task t = new Task();
        t.WhoId    = c.Id;
        t.Subject  = 'Follow up with new contact';
        t.Status   = 'Not Started';
        t.Priority = 'Normal';
        insert t;  // BAD: one DML per loop iteration
    }
}

Correct pattern: collect records, DML once outside the loop

Apex — GOOD DML Pattern RECOMMENDED
// CORRECT — build all records in the loop, one DML statement at the end
trigger ContactTrigger on Contact (after insert) {
    List<Task> tasksToInsert = new List<Task>();

    for (Contact c : Trigger.new) {
        Task t = new Task();
        t.WhoId    = c.Id;
        t.Subject  = 'Follow up with new contact';
        t.Status   = 'Not Started';
        t.Priority = 'Normal';
        tasksToInsert.add(t);  // build the list in memory — no DML yet
    }

    // One DML statement inserts ALL tasks, regardless of batch size
    if (!tasksToInsert.isEmpty()) {
        insert tasksToInsert;
    }
}
ℹ️
DML rows vs. DML statements: A single insert of a List with 5,000 records counts as 1 DML statement but 5,000 DML rows. Both limits apply independently. The 10,000 DML row limit means you can process up to 10,000 records across all DML operations in a single transaction.

Handling partial failures with Database.insert

When you need to tolerate individual record failures without rolling back the entire operation, use Database.insert(records, false) (the allOrNone = false overload). This returns a list of Database.SaveResult objects you can inspect for per-record success/failure information.

Apex — Partial DML with SaveResult RECOMMENDED FOR BULK
List<Database.SaveResult> results =
    Database.insert(tasksToInsert, false);  // allOrNone = false

for (Integer i = 0; i < results.size(); i++) {
    if (!results[i].isSuccess()) {
        for (Database.Error err : results[i].getErrors()) {
            System.debug('Error on record ' + i + ': ' + err.getMessage());
        }
    }
}
Advertisement

5 Using the System.Limits Class

Salesforce exposes the System.Limits class (also referenced without namespace as just Limits) to allow your Apex code to inspect its current resource consumption at runtime. Each limit has two methods: one that returns the amount consumed so far, and one that returns the maximum allowed.

Resource Current Usage Maximum
SOQL queries Limits.getQueries() Limits.getLimitQueries()
SOQL rows returned Limits.getQueryRows() Limits.getLimitQueryRows()
DML statements Limits.getDmlStatements() Limits.getLimitDmlStatements()
DML rows Limits.getDmlRows() Limits.getLimitDmlRows()
Heap size (bytes) Limits.getHeapSize() Limits.getLimitHeapSize()
CPU time (ms) Limits.getCpuTime() Limits.getLimitCpuTime()
Callouts Limits.getCallouts() Limits.getLimitCallouts()
@future calls Limits.getFutureCalls() Limits.getLimitFutureCalls()
Email invocations Limits.getEmailInvocations() Limits.getLimitEmailInvocations()
SOSL queries Limits.getSoslQueries() Limits.getLimitSoslQueries()

Building a diagnostic limit logger

A common pattern is a utility method that logs all current limits for debugging purposes. Call it at the start and end of key operations to understand exactly where your limits are going.

Apex — Limit Diagnostics Utility DEBUGGING AID
public class LimitLogger {

    public static void logAll(String context) {
        System.debug('=== Governor Limits @ ' + context + ' ===');
        System.debug('SOQL Queries  : ' + Limits.getQueries()
            + ' / ' + Limits.getLimitQueries());
        System.debug('SOQL Rows     : ' + Limits.getQueryRows()
            + ' / ' + Limits.getLimitQueryRows());
        System.debug('DML Statements: ' + Limits.getDmlStatements()
            + ' / ' + Limits.getLimitDmlStatements());
        System.debug('DML Rows      : ' + Limits.getDmlRows()
            + ' / ' + Limits.getLimitDmlRows());
        System.debug('Heap Size     : ' + Limits.getHeapSize()
            + ' / ' + Limits.getLimitHeapSize() + ' bytes');
        System.debug('CPU Time      : ' + Limits.getCpuTime()
            + ' / ' + Limits.getLimitCpuTime() + ' ms');
    }

    // Defensive guard — warn if approaching SOQL limit
    public static void guardQueries(Integer threshold) {
        if (Limits.getQueries() >= threshold) {
            System.debug(LoggingLevel.WARN,
                'WARNING: ' + Limits.getQueries()
                + ' of ' + Limits.getLimitQueries()
                + ' SOQL queries consumed!');
        }
    }
}
⚠️
Limits class in production code: Use the Limits class for debugging and test assertions rather than as a runtime gate to skip work. If your code is so close to the limit that it needs a runtime check to decide whether to query, that is a design problem — refactor to reduce usage instead.

6 Bulkification Patterns and Map-Based Lookups

Bulkification is the practice of designing Apex code to handle any number of records — from 1 to 200 in a trigger, or thousands in a batch — using a fixed, small number of SOQL queries and DML statements. It is the single most important skill for writing governor-limit-safe Salesforce code.

The three-phase trigger handler pattern

A well-structured trigger handler separates work into three phases: collect (gather all IDs and field values you will need), query (run all SOQL up front, store in Maps), and process (iterate records, use Map lookups, build lists for DML). This structure guarantees that you never issue SOQL or DML inside a loop.

Apex — Three-Phase Bulkified Handler BEST PRACTICE
public class OpportunityHandler {

    public static void afterUpdate(
        List<Opportunity> newList,
        Map<Id, Opportunity> oldMap
    ) {
        // ===== PHASE 1: COLLECT =====
        Set<Id> accountIds  = new Set<Id>();
        Set<Id> changedOppIds = new Set<Id>();

        for (Opportunity opp : newList) {
            Opportunity oldOpp = oldMap.get(opp.Id);
            // Only process opps where Stage changed to Closed Won
            if (opp.StageName == 'Closed Won'
                    && oldOpp.StageName != 'Closed Won') {
                accountIds.add(opp.AccountId);
                changedOppIds.add(opp.Id);
            }
        }

        if (accountIds.isEmpty()) return;

        // ===== PHASE 2: QUERY (all SOQL here, zero inside loops) =====
        Map<Id, Account> accountMap = new Map<Id, Account>(
            [SELECT Id, Name, OwnerId, Rating
             FROM Account
             WHERE Id IN :accountIds]
        );

        // ===== PHASE 3: PROCESS + COLLECT DML =====
        List<Account> accountsToUpdate = new List<Account>();

        for (Opportunity opp : newList) {
            if (!changedOppIds.contains(opp.Id)) continue;

            Account acc = accountMap.get(opp.AccountId);
            if (acc != null && acc.Rating != 'Hot') {
                acc.Rating = 'Hot';
                accountsToUpdate.add(acc);
            }
        }

        // One DML statement for all updates
        if (!accountsToUpdate.isEmpty()) {
            update accountsToUpdate;
        }
    }
}

Avoiding duplicate DML on the same record

When building lists for DML in a loop, you may add the same record multiple times if multiple opportunities share the same account. Use a Map<Id, SObject> as your accumulator instead of a List — the Map naturally deduplicates by ID, preventing the "DUPLICATE_VALUE" error and unnecessary DML rows.

Apex — Map Deduplication for DML PREVENTS DUPLICATE DML
// Use a Map keyed by Id to avoid duplicate records in DML lists
Map<Id, Account> accountsToUpdateMap = new Map<Id, Account>();

for (Opportunity opp : newList) {
    Account acc = accountMap.get(opp.AccountId);
    if (acc != null) {
        acc.Rating = 'Hot';
        accountsToUpdateMap.put(acc.Id, acc);  // safe — duplicates overwrite
    }
}

if (!accountsToUpdateMap.isEmpty()) {
    update accountsToUpdateMap.values();
}
Bulkification checklist: Before deploying any trigger or batch class, verify: (1) No SOQL inside for loops, (2) No DML inside for loops, (3) All SOQL use WHERE Id IN :set or similar, (4) Results stored in Maps for O(1) lookup, (5) All DML uses Lists/Maps and fires once after the loop.

7 Async Apex and Governor Limits Reset

One of the most powerful tools for working around governor limits is crossing an asynchronous boundary. When your synchronous transaction enqueues a future method, a queueable job, or a batch job, that new async context starts with a fresh, independent set of governor limits. The limit pool is not shared between the sync caller and the async callee.

Async limit increases at a glance

200
SOQL queries (async)
12 MB
Heap size (async)
60 sec
CPU time (async)

Future methods (@future)

Future methods run after the current transaction commits, in a separate async transaction with their own governor limits. They are ideal for making HTTP callouts (which cannot be mixed with DML in the same transaction) and for offloading CPU-heavy work. You can enqueue up to 50 future calls per synchronous transaction.

Apex — @future Method ASYNC PATTERN
public class IntegrationService {

    // callout=true required for HTTP callouts from @future
    @future(callout=true)
    public static void syncToExternalSystem(Set<Id> contactIds) {
        // This runs in a new transaction — fresh 200 SOQL queries, 12 MB heap
        List<Contact> contacts = [
            SELECT Id, FirstName, LastName, Email
            FROM Contact
            WHERE Id IN :contactIds
        ];

        // HTTP callout — allowed because callout=true and no prior DML in this tx
        Http http = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.example.com/contacts');
        req.setMethod('POST');
        // ... build payload and send
    }
}
⚠️
@future limitations: Future methods cannot be called from an already-async context (you get a LimitException if you try). They also cannot return values, and you cannot chain them easily. For complex async workflows, prefer Queueable Apex, which supports chaining and can accept object parameters.

Queueable Apex

Queueable jobs are the preferred replacement for @future methods. They can accept complex object parameters (not just primitives and collections), can be chained (one queueable enqueues another), and can be monitored via AsyncApexJob. A transaction can enqueue up to 50 queueable jobs; within a queueable context, you can only enqueue one child job (preventing infinite chains).

Apex — Queueable with Chaining PREFERRED ASYNC PATTERN
public class ProcessContactsQueueable implements Queueable {

    private List<Id> contactIds;

    public ProcessContactsQueueable(List<Id> ids) {
        this.contactIds = ids;
    }

    public void execute(QueueableContext ctx) {
        // Fresh governor limits: 200 SOQL, 150 DML, 12 MB heap, 60s CPU
        List<Contact> contacts = [
            SELECT Id, Email, LastName
            FROM Contact
            WHERE Id IN :contactIds
        ];

        List<Contact> toUpdate = new List<Contact>();
        for (Contact c : contacts) {
            c.LastName = c.LastName.toUpperCase();
            toUpdate.add(c);
        }
        update toUpdate;

        // Chain next queueable if needed (limit: 1 from async context)
        if (!Test.isRunningTest()) {
            System.enqueueJob(new FollowUpQueueable(contactIds));
        }
    }
}

Batch Apex and the 5-concurrent-job limit

Batch Apex is the right tool for processing millions of records. Each execute() call processes a chunk (default 200, configurable up to 2,000) with its own set of governor limits. The critical operational limit to be aware of is that only 5 batch jobs can be in ACTIVE or PROCESSING state concurrently in a single org. You can have up to 100 jobs in the flex queue (HOLDING state) waiting to run.

ℹ️
Database.getQueryLocator: In the start() method of a batch, using Database.getQueryLocator allows you to process up to 50 million records — far beyond the normal 50,000 SOQL row limit. This is the primary way to iterate over very large data sets without hitting row limits.

8 Governor Limits in Apex Tests

Writing Apex tests that properly account for governor limits is essential for ensuring your code works correctly when deployed to production. There are two key concepts: using Test.startTest() / Test.stopTest() to get a fresh limit pool, and using the Limits class to assert that your code is consuming the expected number of queries and DML statements.

Test.startTest() and Test.stopTest()

Calling Test.startTest() resets all governor limit counters, giving the code between startTest() and stopTest() its own fresh set of limits. This is important for two reasons: it isolates your test setup code (which may consume queries) from the governor limits your actual production code is tested against, and it forces all async operations enqueued between the two calls to complete synchronously when stopTest() is called.

Apex Test — Correct Use of Test.startTest() RECOMMENDED TEST PATTERN
@isTest
private class OpportunityHandlerTest {

    @TestSetup
    static void makeData() {
        // Setup queries/DML here do NOT count against the test's limits
        Account acc = new Account(Name='Test Account');
        insert acc;
        Opportunity opp = new Opportunity(
            Name='Test Opp', AccountId=acc.Id,
            StageName='Prospecting', CloseDate=Date.today().addDays(30)
        );
        insert opp;
    }

    @isTest
    static void testClosedWonUpdatesAccountRating() {
        Opportunity opp = [SELECT Id, StageName FROM Opportunity LIMIT 1];

        Test.startTest();
            // Limits reset here — code under test gets a clean 100 SOQL pool
            opp.StageName = 'Closed Won';
            update opp;
        Test.stopTest();
        // Async jobs (future/queueable) enqueued in startTest complete at stopTest

        // Assertions run after async completes
        Account updatedAcc = [SELECT Rating FROM Account LIMIT 1];
        System.assertEquals('Hot', updatedAcc.Rating,
            'Account Rating should be Hot after Opp closes');
    }
}

Asserting governor limit consumption in tests

Beyond testing functional correctness, you can use the Limits class inside your tests to assert that your bulkified code consumes an expected, bounded number of queries — regardless of the number of records processed. This is how you prove your code is truly bulkified.

Apex Test — Asserting Query Count LIMIT ASSERTION PATTERN
@isTest
static void testBulkTriggerUsesFixedQueryCount() {
    // Insert 200 contacts to simulate a full trigger batch
    Account acc = [SELECT Id FROM Account LIMIT 1];
    List<Contact> contacts = new List<Contact>();
    for (Integer i = 0; i < 200; i++) {
        contacts.add(new Contact(
            LastName='Test ' + i, AccountId=acc.Id
        ));
    }

    Test.startTest();
        insert contacts;  // fires ContactTrigger on 200 records
        Integer queriesUsed = Limits.getQueries();
    Test.stopTest();

    // A properly bulkified trigger uses a small fixed number of queries
    // regardless of batch size — assert it is less than 10, not 200
    System.assert(queriesUsed < 10,
        'Expected <10 queries for bulkified trigger, got: ' + queriesUsed);
}
The bulk test principle: Always test your triggers with 200 records in a single DML operation. A trigger that works with 1 record but fails with 200 is broken. Insert a List of 200 records in your test and assert both functional correctness and that the query count remains bounded and small.

Understanding the @TestSetup annotation

The @TestSetup annotation marks a method that creates shared test data for all test methods in the class. The data is rolled back between tests but re-created fresh for each one. Critically, the SOQL queries and DML statements issued inside @TestSetup do not count against the governor limits of your individual test methods — giving you more headroom to test your production code paths.

ℹ️
Limits in test context: Salesforce test execution enforces governor limits just like production code. If your test itself hits a limit (e.g., 100 SOQL inside test setup), it will fail with a LimitException. Use @TestSetup wisely and ensure your test data creation is efficient.

Practice Governor Limits on ApexArena

Reinforce your understanding of governor limits by solving real Apex coding challenges. ApexArena grades your code for correctness and checks that your solution is properly bulkified.

Frequently Asked Questions

What are Salesforce Governor Limits?
Governor limits are runtime enforcement limits imposed by the Salesforce platform to ensure that no single piece of code monopolises the shared computing resources of the multi-tenant infrastructure. They cap the number of SOQL queries, DML statements, heap memory, CPU time, and other resources that a single Apex transaction can consume. If your code exceeds a limit, Salesforce throws a LimitException and rolls back the entire transaction.
What is the most commonly hit governor limit?
The most frequently hit limit is "101 SOQL queries" — which happens when a SOQL query is placed inside a for loop over records (the classic SOQL in a loop anti-pattern). In a trigger processing 200 records, this instantly fires 200 SOQL queries against the 100-query limit. The fix is to move all SOQL outside the loop, collect the IDs you need, run one bulk query, and store results in a Map for O(1) lookup inside the loop.
What is the CPU time governor limit in Salesforce?
The synchronous CPU time limit is 10,000 milliseconds (10 seconds) per transaction. This counts only the time your Apex code is actually executing on the CPU — not time spent waiting for database queries. For asynchronous contexts (future methods, queueable, batch, scheduled) the limit is 60,000 milliseconds (60 seconds). CPU-intensive string manipulation, complex loops, and recursive algorithms are common culprits. Limits.getCpuTime() returns current CPU milliseconds consumed.
How can you check how close you are to governor limits at runtime?
The System.Limits class exposes every current and maximum governor limit. Key methods: Limits.getQueries() / Limits.getLimitQueries() for SOQL count, Limits.getDmlStatements() / Limits.getLimitDmlStatements() for DML, Limits.getHeapSize() / Limits.getLimitHeapSize() for memory, and Limits.getCpuTime() / Limits.getLimitCpuTime() for CPU milliseconds. You can use these in logging utilities or defensive guards: if (Limits.getQueries() > 90) { log warning; }.
Do governor limits reset between trigger executions?
No — governor limits are per transaction, not per trigger invocation. If your trigger is fired by a workflow rule that then fires another trigger, all those executions share the same limit pool. Limits do reset when you cross an asynchronous boundary: calling a future method, enqueuing a queueable job, or starting a batch gives the new async context its own fresh set of limits. Test.startTest() artificially resets limits in tests for this reason.

More Salesforce Tutorials