Salesforce Apex Governor Limits Per Transaction (2026)
What are the Salesforce Apex governor limits per transaction - SOQL, DML, CPU, heap, callouts?
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.
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)
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]
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.