Salesforce Governor Limits, Best Practices, and Bulkification

Salesforce is a multi-tenant platform — thousands of companies share the same servers. To ensure one company's poorly written code does not slow down or crash the system for everyone else, Salesforce enforces strict Governor Limits. These are hard boundaries on how many operations Apex code can perform in a single transaction. Ignoring governor limits does not just cause bugs — it causes complete failures in production. Understanding and respecting these limits is what separates a beginner from a professional Salesforce developer.

What Is Multi-Tenancy?

  APARTMENT BUILDING = Salesforce Servers
  EACH FLAT          = One Salesforce Org (one company)
  SHARED RESOURCES   = Water, Electricity, Lift

  If one tenant wastes water, everyone suffers.
  Governor Limits = Rules that ensure each tenant uses a fair share.

Key Governor Limits to Know

ResourceSynchronous LimitAsynchronous Limit
SOQL queries100200
Records returned by SOQL50,00050,000
DML statements150150
Records processed by DML10,00010,000
Apex CPU time10,000 ms (10 seconds)60,000 ms (60 seconds)
Heap size (memory)6 MB12 MB
HTTP callouts100100
Future method calls50N/A

A synchronous transaction is code that runs in real time while the user waits — like a trigger on a button click. An asynchronous transaction runs in the background — like a scheduled batch job.

The Most Violated Limits: SOQL and DML in Loops

The two most common governor limit mistakes are putting SOQL queries or DML statements inside loops. This pattern looks innocent but fails catastrophically at scale.

The Bad Pattern

// DANGEROUS: SOQL inside a loop
trigger AccountTrigger on Account (after insert) {
    for (Account acc : Trigger.new) {
        // This query runs once for every Account in the batch
        // If 200 Accounts are inserted, this runs 200 times → LIMIT ERROR
        List<Contact> contacts = [SELECT Id FROM Contact
                                   WHERE AccountId = :acc.Id];
        // process contacts...
    }
}

The Good Pattern: Bulkification

// CORRECT: Collect IDs first, then one SOQL outside the loop
trigger AccountTrigger on Account (after insert) {

    // Step 1: Collect all Account IDs from the trigger batch
    Set<Id> accountIds = new Set<Id>();
    for (Account acc : Trigger.new) {
        accountIds.add(acc.Id);
    }

    // Step 2: One SOQL query for ALL Contacts at once (outside the loop)
    List<Contact> allContacts = [SELECT Id, AccountId
                                  FROM Contact
                                  WHERE AccountId IN :accountIds];

    // Step 3: Process results
    Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
    for (Contact c : allContacts) {
        if (!contactsByAccount.containsKey(c.AccountId)) {
            contactsByAccount.put(c.AccountId, new List<Contact>());
        }
        contactsByAccount.get(c.AccountId).add(c);
    }

    // Now safely process each account's contacts from the map
}

Bulkification means writing code that works correctly whether one record or 200 records are processed at the same time. Salesforce always calls triggers in batches of up to 200 records. Your code must handle the entire batch efficiently.

DML Bulkification

The same rule applies to DML. Collect all records into a list first, then perform a single DML statement on the entire list:

// BAD: DML inside loop
for (Opportunity opp : opportunities) {
    opp.StageName = 'Closed Won';
    update opp;  // One DML per record = hits 150 DML limit fast
}

// GOOD: Single DML on the full list
for (Opportunity opp : opportunities) {
    opp.StageName = 'Closed Won';
}
update opportunities;  // One DML statement for all records

Asynchronous Apex: Escaping Synchronous Limits

When your operation genuinely needs more resources than synchronous limits allow, Salesforce provides asynchronous execution contexts with higher limits and background processing:

Future Methods

A method annotated with @future runs in a separate transaction after the current one completes. Useful for callouts from triggers (callouts are not allowed in synchronous trigger context) and for operations that do not need to complete before the user gets a response.

@future(callout=true)
public static void sendExternalNotification(Id oppId) {
    // HTTP callout to external system
    // Runs in background after the triggering transaction completes
}

Queueable Apex

Queueable Apex is a more flexible version of future methods. It supports chaining (one job triggers the next), accepts complex object parameters, and provides a Job ID for monitoring.

Batch Apex

Batch Apex processes millions of records by splitting them into chunks (default: 200 records per chunk). Each chunk runs as a separate transaction with its own governor limit allocation. Use Batch Apex for large-scale data cleanup, migration, or nightly processing jobs.

global class AccountCleanupBatch implements Database.Batchable<sObject> {

    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, Rating FROM Account WHERE Rating = null'
        );
    }

    global void execute(Database.BatchableContext bc, List<Account> scope) {
        for (Account acc : scope) {
            acc.Rating = 'Warm';
        }
        update scope;
    }

    global void finish(Database.BatchableContext bc) {
        System.debug('Batch job complete.');
    }
}

Scheduled Apex

Scheduled Apex runs a class at a specified time using cron-style scheduling. Use it to kick off a Batch Apex job every night at midnight, for example.

Checking Limits in Code

The Limits class lets you check how many of each resource your current transaction has consumed:

System.debug('SOQL used: ' + Limits.getQueries() + ' of ' + Limits.getLimitQueries());
System.debug('DML used: ' + Limits.getDmlStatements() + ' of ' + Limits.getLimitDmlStatements());
System.debug('CPU used: ' + Limits.getCpuTime() + 'ms of ' + Limits.getLimitCpuTime() + 'ms');

Add these debug statements during development to spot limit issues before they reach production.

Key Best Practices Summary

  • Never put SOQL queries inside loops — collect IDs in a Set, then query with IN outside the loop.
  • Never put DML statements inside loops — collect records in a List, then DML the whole list at once.
  • Assume Trigger.new always holds 200 records — write all trigger logic to handle the full batch.
  • Use asynchronous Apex (Future, Queueable, Batch) when synchronous limits are genuinely insufficient.
  • Use the Limits class during development to monitor resource consumption and catch issues early.

Leave a Comment

Your email address will not be published. Required fields are marked *