Why do governor limits exist?
Real-life examplePicture an apartment society with one shared water tank for 500 flats. If one flat runs every tap all day, the other 499 get nothing. So the society gives each flat a fair share.
Salesforce works the same way. Thousands of companies run on the same servers, which is called multi-tenancy. To keep it fair, Salesforce caps how much any one piece of code can use. Those caps are the governor limits.
If your code crosses a limit, Salesforce stops it immediately and undoes everything that code did. You cannot catch this error with try/catch, so the only real fix is to write code that stays inside the limits.
What is a "transaction"?
Real-life exampleA transaction is one trip to the supermarket. You go in, pick things, pay and leave. Everything you do in that one trip counts together.
In Salesforce, a transaction starts when something runs your code: a user clicks Save, a record is inserted, or an integration calls in. It ends when all the code started by that action finishes. Most limits are counted per transaction.
There are two kinds of code. Synchronous code runs right now while the user waits, like paying at the counter. Asynchronous code runs in the background later, like home delivery. Background code gets bigger limits because nobody is waiting on it.
Every governor limit at a glance
No need to memorise this now. Pick a group, scan it, then read the examples below. Values marked in colour are the bigger asynchronous ones.
The big five limits
Learn these five well and you will avoid almost every governor limit error you will ever see.
SOQL queries
100 per transactionReal-life exampleYour mother sends you to buy 10 things. You do not make 10 separate trips, one item each. You take one list and buy everything in one trip.
Each SOQL query is one trip to the database, and you get 100 trips per transaction.
for (Contact c : contacts) {
// one trip for EVERY contact
Account a = [SELECT Name FROM Account
WHERE Id = :c.AccountId];
}// 1. Write the shopping list
Set<Id> ids = new Set<Id>();
for (Contact c : contacts) ids.add(c.AccountId);
// 2. One trip to the database
Map<Id, Account> accs = new Map<Id, Account>(
[SELECT Name FROM Account WHERE Id IN :ids]);Remember: Never write SOQL inside a loop.
SOQL limit simulator
Move the slider to save more records at once, and see when each style of code breaks.
DML statements
150 per transactionReal-life exampleYou need to post 200 letters. You do not walk to the post office 200 times. You put them all in one bag and go once.
Every insert, update or delete is one trip to the post office.
for (Opportunity o : opps) {
o.StageName = 'Closed Won';
update o; // a trip per record
}for (Opportunity o : opps) {
o.StageName = 'Closed Won';
}
update opps; // one trip for allRemember: Never write insert, update or delete inside a loop.
Too many rows
50,000 read · 10,000 savedReal-life exampleA shopping trolley holds only so much. Even if you go just once, the whole store will not fit in it.
A query like [SELECT Id FROM Case] fails once your org has 80,000 cases. Filter with WHERE to bring back only what you need. For truly big jobs, use Batch Apex: it splits the work into chunks of 200, and each chunk gets fresh limits.
CPU time
10 s sync · 60 s asyncReal-life exampleFinding a friend's number by reading the whole phone book line by line, again for every friend, is slow. Your phone's contact search finds them instantly.
CPU time is how long your own logic runs. Waiting for the database does not count.
for (Opportunity o : opps) { // 5,000
for (Account a : accounts) { // 5,000
if (o.AccountId == a.Id) { }
}
}
// 25 million checksMap<Id, Account> accMap =
new Map<Id, Account>(accounts);
for (Opportunity o : opps) {
Account a = accMap.get(o.AccountId);
}
// 5,000 instant lookupsHeap size (memory)
6 MB sync · 12 MB asyncReal-life exampleTry to carry 500 heavy books in one bag and it tears. Carry 20 at a time and you are fine.
Heap is the memory your variables use. Loading lots of big records - files, long text fields - into one list fills it up. A SOQL for-loop hands you the records 200 at a time instead.
for (List<Account> chunk : [SELECT Id, Description FROM Account]) {
// work with 200 records; they are cleared after
}Remember: Select only the fields you actually use.
Callouts, emails and background jobs
Callouts to APIs
100 calls · 120 s total waitA callout is when Salesforce talks to an outside system, like a payment gateway or an SMS service. Instead of phoning the courier 150 times to book 150 parcels, you send one list.
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_ERP/orders');
req.setMethod('POST');
req.setBody(JSON.serialize(allOrders)); // everything at once
HttpResponse res = new Http().send(req);Sending emails
10 send callsCalling Messaging.sendEmail() in a loop for 15 customers fails on the 11th call. Collect the messages in a list and send them together.
List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
for (Case c : cases) mails.add(buildMail(c));
Messaging.sendEmail(mails);Background jobs
@future 50 · Queueable 50Background jobs are like handing work to a helper so the user does not wait. One transaction can start 50 @future calls and add 50 Queueable jobs, but a job already running in the background can add only 1 more. Call a future method once with all the Ids, not once per record.
@future(callout=true)
public static void syncToErp(Set<Id> accountIds) {
// handle every account here
}Recursion: trigger depth of 16
Real-life exampleTwo mirrors facing each other show an endless tunnel. A trigger that updates its own object does the same, and keeps firing itself.
trigger AccountTrigger on Account (after update) {
List<Account> accs = new List<Account>();
for (Account a : Trigger.new) {
accs.add(new Account(Id = a.Id, Rating = 'Hot'));
}
update accs; // fires this trigger again
}trigger AccountTrigger on Account (before update) {
for (Account a : Trigger.new) {
a.Rating = 'Hot'; // saved with the record, no DML
}
}When you genuinely need an after trigger, remember which records you have already handled, and skip them the second time round.
public class TriggerGuard {
public static Set<Id> done = new Set<Id>();
}
// inside the trigger
List<Account> todo = new List<Account>();
for (Account a : Trigger.new) {
if (TriggerGuard.done.add(a.Id)) todo.add(a); // add() is false if seen
}Remember: Prefer a before trigger. A Set of Ids beats a single true/false flag, which silently skips the second chunk of 200.
Daily limits for the org
Some limits are a daily budget shared by every user, integration and job in your org, like a family mobile data pack. If one app eats it all, everything else stops too.
| Limit | Value | Watch out for |
|---|---|---|
| Background jobs | 250,000 / day | Jobs that spawn 1,000 tasks every 5 minutes |
| Batch jobs | 5 running | Starting batches in a loop |
| Scheduled jobs | 100 | Scheduling one job per record |
| External emails | 5,000 / day | Mass emails sent one by one |
| API calls | by edition | Systems polling Salesforce every second |
Bulk savesA trigger gets records in groups of 200. Insert 1,000 records in one Apex DML statement and the trigger runs 5 times, but all 5 runs share one set of limits. Code using 25 queries per run uses 125 in total, and fails.
Four more things every developer should know
@isTest
static void handles200Contacts() {
List<Contact> cons = new List<Contact>();
for (Integer i = 0; i < 200; i++) cons.add(new Contact(LastName = 'Test ' + i));
Test.startTest(); // fresh limits start here
insert cons;
Test.stopTest();
System.assertEquals(200, [SELECT COUNT() FROM Contact]);
}Golden rules cheat sheet
- No SOQL in loopsCollect Ids first, query once.
- No DML in loopsChange records in a list, save once.
- Use MapsInstant lookups instead of loops in loops.
- Query only what you needFilter with WHERE, pick only the fields you use.
- Big data? Batch ApexSmall chunks, fresh limits each time.
- Slow work? Go asyncQueueable or @future for callouts and heavy logic.
- Stop recursionPrefer before triggers, or track handled Ids.
- Test with 200 recordsIf it works for 200, it works in real life.
Check your usage while coding
The Limits class is your fuel meter. The bottom of every debug log also shows how much of each limit was used.
System.debug('Queries: ' + Limits.getQueries() + ' / ' + Limits.getLimitQueries());
System.debug('DML: ' + Limits.getDmlStatements() + ' / ' + Limits.getLimitDmlStatements());
System.debug('CPU ms: ' + Limits.getCpuTime() + ' / ' + Limits.getLimitCpuTime());Common interview questions
Why does Salesforce have governor limits?
Many companies share the same servers. Limits make sure no single company's code slows down everyone else.
Can I catch a LimitException with try/catch?
No. The transaction stops and everything is rolled back. You have to prevent it, not catch it.
What is bulkification in Salesforce?
Writing code that handles many records at once (up to 200 in a trigger) instead of one at a time, with no queries or saves inside loops.
Why are async governor limits higher?
Background code runs when nobody is waiting, so Salesforce allows more: 200 SOQL queries, 60 seconds of CPU and 12 MB of heap.
How do I process 1 million records in Salesforce?
Use Batch Apex. Its start method can fetch up to 50 million records, and each chunk of 200 gets its own fresh limits.
Do Test.startTest() and Test.stopTest() reset governor limits?
Yes. Code between them gets a fresh set of limits, so test setup does not use up the limits of the code being tested.
Educational only. This article explains how things work. It is not investment, tax, legal or career advice, and it is not a recommendation to buy or sell anything. What suits you depends on your own situation - check with a qualified adviser before acting on money decisions.