Salesforce · Apex for beginners

Salesforce Governor Limits, explained simply

Why your code stops with a LimitException, what every limit means in plain words, and the easy fixes that keep you safe - with real-life examples.

Level: BeginnerRead: 12 minIncludes: code + interactive demo
Bhanu PratapSalesforce Developer · Updated 18 October 2026
The idea

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.

Shared Salesforce serversCompany ACompany BYour companyCompany DEach company gets a fair share. That share is the governor limit.

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.

One word to know

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.

One transaction: every step shares the same limitsUser clicks SaveAccount triggerContacts updatedContact triggerLimits reset only when a new transaction starts.

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.

Reference

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.

SOQL queriesTimes you can read from the databasesync100async200
Rows from SOQLTotal records you can readboth50,000
SOSL searchesText searches you can runboth20
Rows per SOSLRecords one search returnsboth2,000
DML statementsTimes you can save or deleteboth150
Rows via DMLTotal records you can save or deleteboth10,000
CPU timeHow long your code can "think"sync10 sasync60 s
Heap sizeMemory your data can usesync6 MBasync12 MB
CalloutsCalls to outside systems (APIs)both100wait120 s
sendEmail callsTimes you can call the email methodboth10
@future callsBackground tasks you can start (none from inside batch or another future)sync50
Queueable jobsBackground jobs you can addsync50async1
Platform eventsEventBus.publish callsboth150
Trigger depthTriggers firing in a chainboth16
Transaction timeLongest a transaction can runboth10 min
Where 90% of errors come from

The big five limits

Learn these five well and you will avoid almost every governor limit error you will ever see.

1

SOQL queries

100 per transaction

Real-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.

Wrong: query inside a loop
for (Contact c : contacts) {
    // one trip for EVERY contact
    Account a = [SELECT Name FROM Account
                 WHERE Id = :c.AccountId];
}
Right: collect first, query once
// 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]);
Wrong: 200 contacts, 200 queries200 contactsDatabaseFails at query 101: Too many SOQL queriesRight: collect Ids, then 1 query200 contactsSet of Account IdsDatabasecollect1 query

Remember: Never write SOQL inside a loop.

Try it yourself

SOQL limit simulator

Move the slider to save more records at once, and see when each style of code breaks.

Code style
50of 100 queries
Passes
It works today, but it breaks the first time more than 100 records are saved together.
2

DML statements

150 per transaction

Real-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.

Wrong: save inside a loop
for (Opportunity o : opps) {
    o.StageName = 'Closed Won';
    update o;  // a trip per record
}
Right: change all, save once
for (Opportunity o : opps) {
    o.StageName = 'Closed Won';
}
update opps;  // one trip for all

Remember: Never write insert, update or delete inside a loop.

3

Too many rows

50,000 read · 10,000 saved

Real-life exampleA shopping trolley holds only so much. Even if you go just once, the whole store will not fit in it.

Reading 80,000 cases in one query160% of limit

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.

Batch Apex: 1,000,000 recordsChunk 1200 recordsfresh limitsChunk 2200 recordsfresh limits· · ·Chunk 5,000200 recordsfresh limitsEach chunk is its own transaction, so the limits reset every time.
4

CPU time

10 s sync · 60 s async

Real-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.

Wrong: loop inside a loop
for (Opportunity o : opps) {       // 5,000
    for (Account a : accounts) {   // 5,000
        if (o.AccountId == a.Id) { }
    }
}
// 25 million checks
Right: use a Map
Map<Id, Account> accMap =
    new Map<Id, Account>(accounts);
for (Opportunity o : opps) {
    Account a = accMap.get(o.AccountId);
}
// 5,000 instant lookups
5

Heap size (memory)

6 MB sync · 12 MB async

Real-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.

Right: SOQL for-loop, 200 records at a time
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.

Talking to the outside world

Callouts, emails and background jobs

6

Callouts to APIs

100 calls · 120 s total wait

A 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.

Send all records in one request
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);
7

Sending emails

10 send calls

Calling Messaging.sendEmail() in a loop for 15 customers fails on the 11th call. Collect the messages in a list and send them together.

Many emails, one call
List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
for (Case c : cases) mails.add(buildMail(c));
Messaging.sendEmail(mails);
8

Background jobs

@future 50 · Queueable 50

Background 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.

One background task for all records
@future(callout=true)
public static void syncToErp(Set<Id> accountIds) {
    // handle every account here
}
A classic trap

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.

Account is updatedAccountTrigger firesTrigger updates Accountrepeats again and againAfter 16 rounds: Maximum trigger depth exceeded
Wrong: an after trigger that updates 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
}
Fix 1: change the record in a before trigger
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.

Fix 2: remember what already ran
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.

Shared by your whole company

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.

LimitValueWatch out for
Background jobs250,000 / dayJobs that spawn 1,000 tasks every 5 minutes
Batch jobs5 runningStarting batches in a loop
Scheduled jobs100Scheduling one job per record
External emails5,000 / dayMass emails sent one by one
API callsby editionSystems 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.

Often missed

Four more things every developer should know

Flows share the same limitsA record-triggered flow and your Apex trigger on the same save use one shared budget of 100 queries and 150 saves. A heavy flow can push good Apex over the edge.
No callout after a saveInsert or update a record and then call an API in the same transaction, and you get "You have uncommitted work pending". Make the callout first, or move it to a Queueable job.
Slow requests are cappedOnly 10 requests that run longer than 5 seconds can run at once in an org. The 11th fails with ConcurrentPerOrgLongTxn. Move slow work to the background.
Test with fresh limitsCode between Test.startTest() and Test.stopTest() gets a new set of limits. Insert 200 records in your test setup, then run your logic in between to prove it is bulk-safe.
A bulk-safe test
@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]);
}
Save this

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.

Limits class · Apex
System.debug('Queries: ' + Limits.getQueries() + ' / ' + Limits.getLimitQueries());
System.debug('DML: ' + Limits.getDmlStatements() + ' / ' + Limits.getLimitDmlStatements());
System.debug('CPU ms: ' + Limits.getCpuTime() + ' / ' + Limits.getLimitCpuTime());
Interview ready

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.

Practise for the interviewApex Interview Questions and AnswersGovernor limits, triggers, sharing, async Apex and testing - with the answers an interviewer expects, and the follow-ups.

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.