Governor limits are the rules Salesforce enforces to ensure no single org monopolizes shared platform resources. Understanding them is essential — hit them in production and you get exceptions, broken automations, and unhappy users.
Why Governor Limits Exist
Salesforce is a multi-tenant platform. Your org shares compute, database, and API resources with thousands of others. Governor limits prevent any one org from consuming disproportionate resources. They're not bugs — they're the architecture.
The Limits That Actually Bite
Per-Transaction Synchronous Limits
| Limit | Value | Common Trigger | |-------|-------|----------------| | SOQL queries | 100 | SOQL in a loop | | DML statements | 150 | DML in a loop | | DML rows | 10,000 | Bulk operations | | SOQL rows | 50,000 | Retrieving full tables | | Heap size | 6 MB | Storing large datasets | | CPU time | 10,000 ms | Complex loops, algorithms | | Callouts | 100 | External API calls |
Per-Transaction Asynchronous Limits (Batch, Future, Queueable)
| Limit | Value | |-------|-------| | SOQL queries | 200 | | DML statements | 150 | | DML rows | 10,000 | | Heap size | 12 MB (batch) | | CPU time | 60,000 ms |
Org-Wide Daily Limits
| Limit | Value | |-------|-------| | Async Apex executions | 250,000 | | Email alerts | 1,000 per user per day | | Outbound messages | 10,000 | | API calls | Varies by edition (Developer: 15,000/day) |
Monitoring Your Limits
In Apex Code
// Check remaining limits at any point in execution
System.debug('SOQL queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('DML statements: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('DML rows: ' + Limits.getDmlRows() + '/' + Limits.getLimitDmlRows());
System.debug('CPU time: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
System.debug('Heap: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());Proactive Guard Before Critical Operations
public class LimitGuard {
public static Boolean hasSoqlCapacity(Integer needed) {
return (Limits.getLimitQueries() - Limits.getQueries()) >= needed;
}
public static Boolean hasDmlCapacity(Integer needed) {
return (Limits.getLimitDmlStatements() - Limits.getDmlStatements()) >= needed;
}
public static Boolean isHeapSafe(Integer safetyMarginBytes) {
return (Limits.getLimitHeapSize() - Limits.getHeapSize()) > safetyMarginBytes;
}
}
// Usage
if (!LimitGuard.hasSoqlCapacity(5)) {
throw new LimitException('Insufficient SOQL capacity to proceed.');
}The Patterns That Prevent Most Violations
1. Never SOQL or DML Inside a Loop
This is the #1 cause of governor limit violations. One query per record = 201 queries for 201 records.
// ❌ SOQL in loop — hits limit at record 101
for (Opportunity opp : opportunities) {
Account acc = [SELECT Name FROM Account WHERE Id = :opp.AccountId];
// ... use acc
}
// ✅ Bulk-safe pattern
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : opportunities) accountIds.add(opp.AccountId);
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);
for (Opportunity opp : opportunities) {
Account acc = accountMap.get(opp.AccountId);
// ... use acc
}2. Collect and Execute DML Once
// ❌ DML inside a loop
for (Contact c : contacts) {
c.Status__c = 'Processed';
update c; // 1 DML per record
}
// ✅ Collect then DML once
List<Contact> toUpdate = new List<Contact>();
for (Contact c : contacts) {
c.Status__c = 'Processed';
toUpdate.add(c);
}
if (!toUpdate.isEmpty()) update toUpdate; // 1 DML total3. Use @future or Queueable for Callouts
Callouts can't be made from DML-opened transactions synchronously in some contexts. Offload them:
public class ExternalSyncService {
@future(callout=true)
public static void syncToERP(List<Id> accountIds) {
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];
// ... make HTTP callouts
}
}
// Trigger invocation
trigger AccountTrigger on Account (after insert) {
ExternalSyncService.syncToERP(
new List<Id>(new Map<Id, Account>(Trigger.new).keySet())
);
}4. Use Aggregate SOQL to Reduce Row Counts
// ❌ Retrieve 50k rows just to count them
List<Opportunity> all = [SELECT Id FROM Opportunity WHERE AccountId = :accId];
Integer count = all.size(); // uses 50k of your 50k row limit!
// ✅ Aggregate at the database level
Integer count = [SELECT COUNT() FROM Opportunity WHERE AccountId = :accId];5. Manage Heap with Null Disposal
public void execute(Database.BatchableContext bc, List<Account> scope) {
// Process first half
processPhaseOne(scope);
// Explicitly null large intermediate data to free heap before phase 2
// (Salesforce GC will reclaim it during next allocation)
List<Account> processed = null;
// Process second phase with more heap available
processPhaseTwo(scope);
}Queueable Apex: Better Than @future
Queueable jobs are the modern replacement for @future — they support chaining and type-safe parameters:
public class SyncQueueable implements Queueable, Database.AllowsCallouts {
private List<Id> recordIds;
public SyncQueueable(List<Id> ids) {
this.recordIds = ids;
}
public void execute(QueueableContext ctx) {
// Do work — can make callouts (Database.AllowsCallouts)
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :recordIds];
// ... process
// Chain next job if more work to do
if (moreWorkNeeded()) {
System.enqueueJob(new SyncQueueable(nextBatch));
}
}
}
// Enqueue
System.enqueueJob(new SyncQueueable(accountIds));Limit: Max 50 chained Queueable jobs per transaction. Max 2 Queueable jobs enqueued from a single synchronous transaction.
What To Do When You Hit a Limit
LimitException in Production
- Check debug logs — identify which line threw the exception
- Find the root cause — usually a trigger calling a method that does SOQL/DML in a loop
- Check recursion — is the trigger calling itself? Use a static recursion guard
- Refactor — apply bulkification patterns
- Consider async — move heavy processing to Batch or Queueable
Common Scenario: Trigger + Process Builder Double-Fire
// A trigger fires → Process Builder creates records → trigger fires again
// Solution: static recursion guard
public class TriggerRecursionGuard {
private static Boolean running = false;
public static Boolean isRunning() { return running; }
public static void setRunning() { running = true; }
}
// In trigger handler
if (TriggerRecursionGuard.isRunning()) return;
TriggerRecursionGuard.setRunning();
// ... your logicDeveloper Console Limit Tracking
In Developer Console → Logs → open a debug log → click Execution Overview tab.
You'll see a chart of governor limit consumption throughout the transaction — useful for identifying which method is consuming the most SOQL queries or heap.
Summary Table: Anti-Patterns → Solutions
| Anti-Pattern | Problem | Solution |
|--------------|---------|----------|
| SOQL in loop | 100 query limit | Collect IDs, query once with IN |
| DML in loop | 150 DML limit | Collect records, DML once |
| Large SOQL without LIMIT | 50,000 row limit | Add WHERE, LIMIT, or use aggregate |
| Trigger recursion | Infinite loop | Static recursion guard class |
| Synchronous callout | CPU/callout limit | @future or Queueable + AllowsCallouts |
| Storing full SObject lists | 6MB heap | Use ID sets/maps instead |
Conclusion
Governor limits force you to write better code — bulkified, efficient, and mindful of resource consumption. The patterns here (bulk queries, single DML, async callouts, aggregate SOQL) are not workarounds — they're the foundation of professional Salesforce development. Apply them consistently and you'll rarely see a LimitException outside of tests.
Useful Resources: