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.
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
// 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
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
60s for async. Counts only Apex execution, not SOQL wait time.
12 MB for async. Total memory for all objects in transaction.
// ❌ 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
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.
// @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) {}
}