Governor Limits & Bulkification
The limits every interviewer asks about and the before/after code patterns that keep you under them.
Developer 3 min readGovernor limitsBulkificationPerformance
Salesforce is multi-tenant, so every transaction gets a fixed budget. Exceed it and you get an uncatchable System.LimitException — the whole transaction rolls back.
Per-transaction limits (most asked)
| Limit | Synchronous | Asynchronous |
|---|---|---|
| SOQL queries | 100 | 200 |
| Records retrieved by SOQL | 50,000 | 50,000 |
| DML statements | 150 | 150 |
| Records processed by DML | 10,000 | 10,000 |
| CPU time | 10,000 ms | 60,000 ms |
| Heap size | 6 MB | 12 MB |
| Callouts | 100 | 100 |
| SOSL queries | 20 | 20 |
@future calls | 50 | 0 in batch/future |
Check usage at runtime with the Limits class: Limits.getQueries() / Limits.getLimitQueries().
Triggers receive up to 200 records at a time
A single DML of 1,000 records fires your trigger 5 times (chunks of 200) in the same transaction, and all chunks share one set of limits. Code that's fine for 1 record explodes in bulk. (Data Loader / the Bulk API send batches; each batch is its own transaction.)
Anti-pattern: SOQL / DML inside a loop
apex
// ❌ 1 query + 1 DML per record → fails at the 101st record
for (Opportunity opp : Trigger.new) {
Account acc = [SELECT Id, Type FROM Account WHERE Id = :opp.AccountId];
acc.Type = 'Customer';
update acc;
}
Bulkified version
apex
// ✅ 0 queries, 1 DML for any number of records
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : Trigger.new) {
Opportunity old = Trigger.oldMap.get(opp.Id);
if (opp.StageName == 'Closed Won' && old.StageName != 'Closed Won' && opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
List<Account> toUpdate = new List<Account>();
for (Id accId : accountIds) {
toUpdate.add(new Account(Id = accId, Type = 'Customer'));
}
if (!toUpdate.isEmpty()) update toUpdate;
The bulkification checklist
- Collect keys (Ids, emails, names) into a
Setin one loop. - Query once with
WHERE … IN :set, put results in aMap. - Loop again and look up from the Map.
- Collect records to change into a
List. - One DML outside the loop (and skip it when the list is empty).
Other ways to stay under limits
- Query only the fields you need; use
WHEREfilters on indexed fields (Id, Name, lookups, External IDs). - Use SOQL
forloops for big result sets (for (List<Account> chunk : [SELECT …])) to reduce heap. - Move heavy work to async Apex (Queueable / Batch) for higher limits.
- Avoid recursion — a trigger updating its own object re-fires itself.
Interview questions
- Your trigger works in the UI but fails during a 5,000-row data load — why? → It isn't bulkified (SOQL/DML in loops).
- How many records does a trigger get per invocation? → Up to 200.
- Can you catch a governor limit exception? → No — prevent it.
- How do you check how many queries you've used? →
Limits.getQueries().