home Home build Tools bug_report Errors menu_book Guides lightbulb Tips smart_toy Prompts extension Extensions folder_open Resources info About
search

Salesforce is a multi-tenant platform — thousands of customers share the same infrastructure. Governor limits are enforced guardrails that prevent any single org or transaction from monopolizing shared resources. Understanding them isn't optional; it's fundamental to Salesforce development.

Why Governor Limits Exist

In a traditional single-tenant application, you can consume as many resources as your server allows. In Salesforce's multi-tenant model, the platform must guarantee fair resource distribution across all orgs running on the same servers at the same time. Governor limits are the technical implementation of that guarantee.

info

Limits are per transaction: Every Apex transaction (trigger, class method, batch job chunk, future method) starts with a fresh set of limits. Async operations like @future and Batch Apex get their own separate limit context.

SOQL & SOSL Limits

Limit Synchronous Asynchronous
SOQL Queries 100 200
Records Retrieved by SOQL 50,000 50,000
SOSL Queries 20 20
Records Retrieved by SOSL 2,000 2,000
Apex — Monitor SOQL Usage
// Check limit consumption in real-time
System.debug('SOQL used: ' + Limits.getQueries() + ' / ' + Limits.getLimitQueries());
System.debug('Rows used: ' + Limits.getQueryRows() + ' / ' + Limits.getLimitQueryRows());

// Proactive check before a large query
if (Limits.getQueries() >= Limits.getLimitQueries() - 5) {
    // Too close to limit — defer to async or throw a handled exception
    throw new LimitException('Approaching SOQL limit, cannot proceed synchronously');
}

DML Limits

Limit Synchronous Asynchronous
DML Statements 150 150
Records via DML 10,000 10,000
warning

One DML, many records: insert myList is ONE DML statement regardless of list size. Each separate insert, update, delete, upsert, or merge call counts as one statement against the 150 limit.

CPU Time & Heap Size

10s
CPU time (synchronous)

60s for async. Counts only Apex execution, not SOQL wait time.

6 MB
Heap size (synchronous)

12 MB for async. Total memory for all objects in transaction.

Apex — CPU & Heap Optimization Tips
// ❌ Heap hog — stores entire query result in memory
List<Account> accounts = [SELECT Id, Name, Description FROM Account];

// ✅ Only fetch fields you need — reduces heap usage
List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 1000];

// ❌ CPU hog — string concatenation in loops
String result = '';
for (Integer i = 0; i < 10000; i++) {
    result += i + ','; // Creates new String object each iteration
}

// ✅ Use List and join — far more CPU efficient
List<String> parts = new List<String>();
for (Integer i = 0; i < 10000; i++) {
    parts.add(String.valueOf(i));
}
String result = String.join(parts, ',');

// Monitor mid-execution
System.debug('CPU used: ' + Limits.getCpuTime() + 'ms');
System.debug('Heap used: ' + Limits.getHeapSize() + ' bytes');

Callout Limits

Limit Value
Callouts per transaction 100
Callout timeout (max) 120 seconds
Response body size 6 MB
error

Critical Rule: You cannot make a callout after a DML operation in the same transaction. If you do, you'll get System.CalloutException: You have uncommitted work pending. Use @future(callout=true) or Queueable to separate them.

Async Context Limits

Moving work to asynchronous contexts is the primary strategy for working around synchronous limits. Each async mechanism has its own limit context.

Apex — Async Limit Contexts
// @future — simple async, higher limits, no chaining
public class AsyncHelper {
    @future(callout=true)
    public static void doCallout(Set<Id> recordIds) {
        // 200 SOQL, 200 DML, 12MB heap, 60s CPU
        // No further @future calls from here
    }
}

// Queueable — chainable, can store state, supports callouts
public class MyQueueable implements Queueable, Database.AllowsCallouts {
    private List<Id> recordIds;

    public MyQueueable(List<Id> ids) { this.recordIds = ids; }

    public void execute(QueueableContext ctx) {
        // Process first batch
        // Chain the next batch if more remain
        if (!recordIds.isEmpty()) {
            System.enqueueJob(new MyQueueable(remainingIds));
        }
    }
}

// Batch Apex — best for LDV, 200 records per chunk
public class MyBatch implements Database.Batchable<SObject> {
    public Database.QueryLocator start(Database.BatchableContext bc) {
        // Can query up to 50 million records (not subject to SOQL row limit)
        return Database.getQueryLocator([SELECT Id FROM Contact]);
    }
    public void execute(Database.BatchableContext bc, List<Contact> scope) {
        // Fresh limits per chunk of up to 200 records
    }
    public void finish(Database.BatchableContext bc) {}
}

Full Limits Reference

100
SOQL queries (sync)
150
DML statements
6 MB
Heap size (sync)
10s
CPU time (sync)
50K
SOQL rows returned
100
HTTP callouts
10K
DML records
50M
Batch query locator rows