Salesforce Apex Governor Limits Per Transaction (2026)
Type: ERP Integration
System: Salesforce Platform (API v63.0)
Confidence: 0.95
Sources: 7
Verified: 2026-03-02
Freshness: evolving
TL;DR
- Bottom line: Salesforce enforces per-transaction governor limits on every Apex execution context -- SOQL, DML, CPU, heap, and callouts all have hard caps that abort the transaction when exceeded.
- Key limit: 100 SOQL queries per synchronous transaction (200 async), shared across all triggers, flows, and process builders in the same execution context. [src1]
- Watch out for: Governor limits are per-transaction, not per-trigger -- cascading triggers, workflow rules, and process builders all consume from the same pool. A single DML operation can fire multiple triggers that collectively exhaust the 100-SOQL or 10,000 ms CPU budget. [src1]
- Best for: Reference card for any Apex developer, integration architect, or AI agent recommending Salesforce customization patterns.
- Authentication: N/A -- governor limits apply to all Apex execution regardless of authentication method.
System Profile
This card covers Salesforce Apex governor limits as enforced by the Salesforce multitenant platform. These limits apply to all Apex code execution -- triggers, classes, Visualforce controllers, Lightning components, @future methods, Batch Apex, Queueable Apex, and Scheduled Apex. Limits apply uniformly across Enterprise, Unlimited, Performance, and Developer editions; edition differences affect org-level API call quotas (covered separately) but not per-transaction governor limits.
| Property | Value |
| Vendor | Salesforce |
| System | Salesforce Platform (Spring '26, API v63.0) |
| API Surface | Apex Runtime (triggers, classes, VF, Lightning, async Apex) |
| Current API Version | v63.0 (Spring '26) |
| Editions Covered | Enterprise, Unlimited, Performance, Developer |
| Deployment | Cloud |
| API Docs | Execution Governors and Limits |
| Status | GA |
Rate Limits & Quotas
Per-Transaction Governor Limits (Core)
| Limit Type | Synchronous | Asynchronous | Notes |
| SOQL queries | 100 | 200 | Includes queries from triggers, flows, process builders in same context |
| Records retrieved by SOQL | 50,000 | 50,000 | Use LIMIT clause or queryMore/Database.getQueryLocator for larger sets |
| SOSL queries | 20 | 20 | Each query returns max 2,000 records |
| Records retrieved by SOSL | 2,000 | 2,000 | Per single SOSL query |
| DML statements | 150 | 150 | Each insert/update/delete/undelete counts as 1, regardless of record count |
| Records processed by DML | 10,000 | 10,000 | Total across all DML statements in the transaction |
| CPU time | 10,000 ms | 60,000 ms | Exceeded = System.LimitException, transaction aborted |
| Heap size | 6 MB | 12 MB | Collections, deserialized JSON, SOQL results all consume heap |
| HTTP callouts | 100 | 100 | Includes REST, SOAP, and any external HTTP request |
| Callout cumulative timeout | 120 seconds | 120 seconds | Total across all callouts in the transaction |
| Default callout timeout | 10 seconds | 10 seconds | Configurable per request up to 120 seconds |
| Future method calls (@future) | 50 | 0 (batch/future) / 1 (queueable) | Cannot call @future from @future or batch |
| Queueable jobs (System.enqueueJob) | 50 | 1 | Queueable can chain to 1 more queueable |
| sendEmail invocations | 10 | 10 | Per Messaging.sendEmail call |
| Push notification method calls | 10 | 10 | Each call can send up to 2,000 notifications |
| Maximum execution time | 10 minutes | 10 minutes | Hard timeout regardless of CPU usage |
| Trigger recursion depth | 16 | 16 | Maximum nested trigger invocations |
Certified Managed Package Limits
Certified managed packages (AppExchange) receive their own separate governor limit pools in addition to the org's native code limits. [src1]
| Limit Type | Additional Allocation (per package) |
| SOQL queries | +100 (sync) / +100 (async) |
| DML statements | +150 |
| Callouts | +100 |
| sendEmail | +10 |
| Database.getQueryLocator records | +10,000 |
| SOSL queries | +20 |
Org-Level Async Apex Limits (24-hour rolling)
| Limit Type | Value | Notes |
| Asynchronous Apex executions | 250,000 or (user licenses x 200), whichever is greater | Shared across batch, queueable, scheduled, future |
| Concurrent batch jobs (queued + active) | 5 | Additional jobs queue until slot opens |
| Scheduled Apex classes | 100 (Developer: 5) | |
| Synchronous concurrent transactions (long-running) | 10 | Transactions running >5 seconds |
| Batch Apex Database.QueryLocator records | 50,000,000 | Per batch job |
| Batch size (Database.executeBatch) | 200 records (default) | Configurable 1-2,000 |
Org-Level API Request Limits (24-hour rolling)
These are NOT governor limits -- they are org-level quotas on API calls. Included because agents frequently confuse them. [src3]
| Edition | Base API Calls/24h | Per User License | Per Platform License | Sandbox |
| Developer | 15,000 | -- | -- | -- |
| Enterprise | 100,000 | +1,000 | +1,000 | 5,000,000 |
| Unlimited | 100,000 | +5,000 | +5,000 | 5,000,000 |
| Performance | 100,000 | +5,000 | +5,000 | 5,000,000 |
Formula: base + (user_licenses x per_license) + purchased_add_ons
Constraints
- Limits are per-transaction, not per-trigger: A single DML operation can fire before-update triggers, after-update triggers, workflow rules, process builders, and flows -- all sharing the same 100-SOQL, 150-DML, and 10,000 ms CPU pool. [src1]
- No catching LimitException: When a governor limit is exceeded, Salesforce throws System.LimitException, which cannot be caught by try-catch blocks. The entire transaction is rolled back. [src1]
- @future cannot chain: Future methods cannot call other future methods. Use Queueable Apex for async chaining (max depth: 1 queueable per async context). [src1]
- Callout restrictions in triggers: You cannot make HTTP callouts directly from a trigger. All callouts from trigger context must be delegated to @future or Queueable methods. [src1]
- Mixed DML restrictions: You cannot perform DML on setup objects (User, Group, etc.) and non-setup objects in the same transaction. Use @future or System.runAs() to separate contexts. [src1]
- Heap is not just variables: Heap includes SOQL query results held in memory, deserialized JSON/XML, and any object reference. A single query returning 50,000 rows of a large object can exhaust 6 MB heap instantly. [src4]
Integration Pattern Decision Tree
START -- Developer hitting or worried about governor limits
|-- Which limit is being hit?
| |-- SOQL queries (100 sync / 200 async)
| | |-- SOQL inside a loop?
| | | |-- YES -> Bulkify: query ONCE before loop, use Map<Id, SObject> for lookups
| | | +-- NO -> Check for cascading triggers consuming SOQL budget
| | +-- Need >50,000 rows?
| | |-- YES -> Use Database.getQueryLocator in Batch Apex (50M record limit)
| | +-- NO -> Add LIMIT clause, use selective filters (indexed fields)
| |-- DML statements (150)
| | |-- DML inside a loop?
| | | |-- YES -> Collect records in a List, single DML outside loop
| | | +-- NO -> Check for process builders/flows adding DML statements
| | +-- Hitting 10,000 DML rows?
| | +-- Move to Batch Apex (processes records in chunks of 200)
| |-- CPU time (10,000 ms sync)
| | |-- Complex logic in trigger?
| | | |-- YES -> Move heavy processing to @future or Queueable
| | | +-- NO -> Profile with Limits.getCpuTime() to find hotspot
| | +-- Need >10s processing?
| | +-- Use Batch Apex (60,000 ms per execute()) or Platform Events
| |-- Heap size (6 MB sync)
| | |-- Large SOQL result set?
| | | |-- YES -> Use FOR loop on SOQL (streaming), or reduce fields in SELECT
| | | +-- NO -> Check for large String/JSON operations
| | +-- Need >6 MB?
| | +-- Move to async context (12 MB) or Batch Apex
| +-- Callouts (100 per transaction)
| |-- Callouts in a trigger?
| | |-- YES -> MUST use @future or Queueable (triggers block callouts)
| | +-- NO -> Batch external calls, use Composite API to reduce count
| +-- Need >100 callouts?
| +-- Use Batch Apex with Database.AllowsCallouts
+-- General approach
|-- < 200 records -> Synchronous trigger/class (standard limits)
|-- 200-10,000 records -> Queueable Apex (async limits, chainable)
|-- > 10,000 records -> Batch Apex (50M record ceiling)
+-- Real-time notifications -> Platform Events (separate transaction context)
Quick Reference
| Operation | Sync Limit | Async Limit | How to Check Remaining |
| SOQL queries issued | 100 | 200 | Limits.getQueries() / Limits.getLimitQueries() |
| SOQL rows retrieved | 50,000 | 50,000 | Limits.getQueryRows() / Limits.getLimitQueryRows() |
| DML statements issued | 150 | 150 | Limits.getDmlStatements() / Limits.getLimitDmlStatements() |
| DML rows processed | 10,000 | 10,000 | Limits.getDmlRows() / Limits.getLimitDmlRows() |
| CPU time consumed | 10,000 ms | 60,000 ms | Limits.getCpuTime() / Limits.getLimitCpuTime() |
| Heap size used | 6 MB | 12 MB | Limits.getHeapSize() / Limits.getLimitHeapSize() |
| Callouts made | 100 | 100 | Limits.getCallouts() / Limits.getLimitCallouts() |
| Future calls made | 50 | 0-1 | Limits.getFutureCalls() / Limits.getLimitFutureCalls() |
| Queueable jobs added | 50 | 1 | Limits.getQueueableJobs() / Limits.getLimitQueueableJobs() |
| SOSL queries issued | 20 | 20 | Limits.getSoslQueries() / Limits.getLimitSoslQueries() |
| Email invocations | 10 | 10 | Limits.getEmailInvocations() / Limits.getLimitEmailInvocations() |
Code Examples
Apex: Checking Governor Limit Consumption at Runtime
// Input: Running Apex code that needs to monitor limit consumption
// Output: Debug log with current consumption vs limits
public class GovernorLimitChecker {
public static void logLimits(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('CPU time: ' + Limits.getCpuTime() + ' ms / ' + Limits.getLimitCpuTime() + ' ms');
System.debug('Heap size: ' + Limits.getHeapSize() + ' / ' + Limits.getLimitHeapSize());
System.debug('Callouts: ' + Limits.getCallouts() + ' / ' + Limits.getLimitCallouts());
System.debug('Future calls: ' + Limits.getFutureCalls() + ' / ' + Limits.getLimitFutureCalls());
}
public static Boolean canAffordQuery(Integer needed) {
return (Limits.getLimitQueries() - Limits.getQueries()) >= needed;
}
public static Boolean canAffordDml(Integer needed) {
return (Limits.getLimitDmlStatements() - Limits.getDmlStatements()) >= needed;
}
}
Apex: Bulkified Trigger Pattern (Avoids SOQL/DML in Loops)
// Input: Trigger on Account -- up to 200 records per batch
// Output: Related Contacts updated without hitting governor limits
trigger AccountTrigger on Account (after update) {
Set<Id> accountIds = new Set<Id>();
for (Account acc : Trigger.new) {
Account oldAcc = Trigger.oldMap.get(acc.Id);
if (acc.BillingCity != oldAcc.BillingCity) {
accountIds.add(acc.Id);
}
}
if (accountIds.isEmpty()) return;
// 1 SOQL query regardless of batch size (uses 1 of 100)
List<Contact> contactsToUpdate = [
SELECT Id, MailingCity, AccountId
FROM Contact
WHERE AccountId IN :accountIds
];
for (Contact c : contactsToUpdate) {
c.MailingCity = Trigger.newMap.get(c.AccountId).BillingCity;
}
// 1 DML statement regardless of record count (uses 1 of 150)
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
cURL: Check Org-Level API Limits
# Input: Valid access token and instance URL
# Output: JSON with current API usage and remaining limits
# Check org-level API usage via REST API
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"$INSTANCE_URL/services/data/v63.0/limits" | jq '.DailyApiRequests'
# Expected output:
# { "Max": 100000, "Remaining": 98500 }
# Check all org limits (async Apex, bulk API, etc.)
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"$INSTANCE_URL/services/data/v63.0/limits" | jq '.'
Anti-Patterns
Wrong: SOQL Query Inside a Loop
// BAD -- consumes 1 SOQL per record. 200 records = 200 SOQL = LimitException
for (Account acc : Trigger.new) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
// Process contacts...
}
Correct: Single Query with Collection Filter
// GOOD -- 1 SOQL regardless of batch size
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :Trigger.newMap.keySet()]) {
if (!contactsByAccount.containsKey(c.AccountId)) {
contactsByAccount.put(c.AccountId, new List<Contact>());
}
contactsByAccount.get(c.AccountId).add(c);
}
for (Account acc : Trigger.new) {
List<Contact> contacts = contactsByAccount.get(acc.Id);
// Process contacts...
}
Wrong: DML Inside a Loop
// BAD -- 200 records = 200 DML statements. Exceeds 150 DML limit.
for (Contact c : contactsToUpdate) {
c.MailingCity = 'New York';
update c; // 1 DML per iteration
}
Correct: Collect and Batch DML
// GOOD -- 1 DML statement regardless of collection size
List<Contact> toUpdate = new List<Contact>();
for (Contact c : contactsToUpdate) {
c.MailingCity = 'New York';
toUpdate.add(c);
}
update toUpdate; // 1 DML, even for 10,000 records
Wrong: Synchronous Callouts in Triggers
// BAD -- callouts are blocked in trigger context; throws CalloutException
trigger OrderTrigger on Order (after insert) {
for (Order o : Trigger.new) {
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('https://erp.example.com/api/orders');
req.setMethod('POST');
HttpResponse res = h.send(req); // CalloutException!
}
}
Correct: Delegate Callouts to @future or Queueable
trigger OrderTrigger on Order (after insert) {
Set<Id> orderIds = new Set<Id>();
for (Order o : Trigger.new) {
orderIds.add(o.Id);
}
OrderCalloutService.syncToErp(orderIds);
}
public class OrderCalloutService {
@future(callout=true)
public static void syncToErp(Set<Id> orderIds) {
List<Order> orders = [SELECT Id, OrderNumber, TotalAmount FROM Order WHERE Id IN :orderIds];
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('https://erp.example.com/api/orders');
req.setMethod('POST');
req.setBody(JSON.serialize(orders));
req.setTimeout(30000);
HttpResponse res = h.send(req);
}
}
Common Pitfalls
- Trigger recursion consuming the SOQL pool: A trigger on Account updates Contacts, which fires a Contact trigger that queries Accounts again. Each cascade consumes from the same 100-SOQL budget. Fix:
Use static Boolean flags to prevent re-entry, or implement a trigger framework with recursion guards. [src1]
- Process Builder + Flow + Trigger stacking: A single record save can fire a before trigger, after trigger, process builder, workflow rule, and flow -- all in the same transaction. Each consumes governor limits from the same pool. Fix:
Audit all automations on the object. Consolidate to a single automation framework. [src5]
- Large JSON deserialization blowing heap: Deserializing a 5 MB API response in synchronous context exhausts the 6 MB heap because the original String + deserialized objects both occupy heap simultaneously. Fix:
Move to async context (12 MB heap) or parse streaming with JSON.createParser(). [src4]
- SELECT * equivalent: Using SELECT with all fields on objects wastes SOQL rows and heap. Fix:
Select only the fields you need. Use FieldSet or dynamic SOQL for configurable field lists. [src5]
- Not using Database.Stateful in Batch: Without Database.Stateful, instance variables reset between execute() calls. Developers add extra SOQL queries to re-fetch state. Fix:
Implement Database.Stateful to persist state across batch chunks. [src1]
- Assuming sandbox has same limits as production: Developer Edition sandboxes have different org-level limits (15,000 API calls vs 100,000+). Fix:
Use a Full Sandbox or Partial Copy Sandbox for load testing. [src6]
Diagnostic Commands
// Check current governor limit consumption in Execute Anonymous
System.debug('SOQL: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('SOQL Rows: ' + Limits.getQueryRows() + '/' + Limits.getLimitQueryRows());
System.debug('DML: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('DML Rows: ' + Limits.getDmlRows() + '/' + Limits.getLimitDmlRows());
System.debug('CPU: ' + Limits.getCpuTime() + 'ms/' + Limits.getLimitCpuTime() + 'ms');
System.debug('Heap: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());
System.debug('Callouts: ' + Limits.getCallouts() + '/' + Limits.getLimitCallouts());
System.debug('Future: ' + Limits.getFutureCalls() + '/' + Limits.getLimitFutureCalls());
System.debug('Queueable: ' + Limits.getQueueableJobs() + '/' + Limits.getLimitQueueableJobs());
# Query Apex execution logs for limit violations (via Tooling API)
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"$INSTANCE_URL/services/data/v63.0/tooling/query/?q=SELECT+Id,Operation,Status,LogLength+FROM+ApexLog+WHERE+Status='Error'+ORDER+BY+SystemModstamp+DESC+LIMIT+10"
Version History & Compatibility
| API Version | Release | Status | Governor Limit Changes | Notes |
| v63.0 | Spring '26 (Feb 2026) | Current | No governor limit changes | Named Query API GA |
| v62.0 | Winter '26 (Oct 2025) | Supported | No governor limit changes | -- |
| v61.0 | Summer '25 (Jun 2025) | Supported | No governor limit changes | -- |
| v60.0 | Spring '25 (Feb 2025) | Supported | No governor limit changes | -- |
| v59.0 | Winter '25 (Oct 2024) | Supported | No governor limit changes | -- |
| v58.0 | Spring '24 (Feb 2024) | Supported | CPU time measurement methodology updated | Last significant limit-related change |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
| Writing Apex triggers, classes, or controllers | Need org-level API call quotas per edition | Salesforce API request limits reference |
| Debugging LimitException errors in production | Need Bulk API batch size and file limits | Salesforce Bulk API 2.0 reference |
| Designing trigger architecture for high-volume objects | Need authentication flow guidance | Salesforce auth flows reference |
| Planning async processing strategy (batch vs queueable) | Need NetSuite or SAP transaction limits | System-specific governor limits card |
| Reviewing AppExchange package limit isolation | Need to understand Flow-specific limits (not Apex) | Salesforce Flow limits reference |
Important Caveats
- Governor limits are per-transaction but the "transaction" includes ALL automation on the object -- triggers, process builders, flows, workflow rules, and validation rules all share the same limit pool. Audit everything before assuming you have budget.
- Certified managed packages from AppExchange get their own separate governor limit pools. However, unmanaged packages share the org's limits. Do not confuse the two.
- The Limits class methods (e.g.,
Limits.getQueries()) show consumption within the current transaction only. They reset to zero for each new transaction.
- Salesforce can impose additional "concurrent request" throttling during peak load independent of governor limits. This manifests as "Unable to process request" errors even when governor limits are not exceeded.
- These limits are current as of Spring '26 (API v63.0). Salesforce rarely decreases limits but may adjust measurement methodology (as with CPU time in Spring '24). Always verify against the official Apex Developer Guide for your API version.
Related Units