Bulkification & Governor Limits

The full table appears in Appendix §44.3.

21.1 Key per-transaction limits

SOQL queries issued Records retrieved by SOQL Records processed by DML Callouts (HTTP/web service) Cumulative callout timeout @future calls 0 in future/batch (50 in Queueable)

Queueable jobs enqueued (System.enqueueJob) sendEmail invocations

Event publishes (EventBus.publish calls, immediate events) Trigger recursion depth (stack depth)

The full table appears in Appendix §44.3.

21.2 The Limits class

System.debug('SOQL: ' + Limits.getQueries() + ' / ' + Limits.getLimitQueries());
System.debug('DML:  ' + Limits.getDmlStatements() + ' / ' + Limits.getLimitDmlStatements());
System.debug('Rows: ' + Limits.getDmlRows() + ' / ' + Limits.getLimitDmlRows());
System.debug('CPU:  ' + Limits.getCpuTime() + ' / ' + Limits.getLimitCpuTime());
System.debug('Heap: ' + Limits.getHeapSize() + ' / ' + Limits.getLimitHeapSize());
System.debug('Callouts: ' + Limits.getCallouts() + ' / ' + Limits.getLimitCallouts());
System.debug('Queueables: ' + Limits.getQueueableJobs() + ' / ' + Limits.getLimitQueueableJobs());

if (Limits.getQueries() > Limits.getLimitQueries() - 5) {
    // degrade gracefully: hand the rest of the work to an async job
}

21.3 Bulkification patterns

Anti-pattern: SOQL and DML in loops Pattern: collect → query once → map → process → DML once

// BAD — 1 query + 1 DML per record → fails above 100 records
for (Contact c : Trigger.new) {
    Account a = [SELECT Id, Name FROM Account WHERE Id = :c.AccountId];
    a.Last_Contact_Added__c = Date.today();
    update a;
}
// GOOD — 1 query + 1 DML no matter how many records
Set<Id> accountIds = new Set<Id>();
for (Contact c : (List<Contact>) Trigger.new) {
    if (c.AccountId != null) accountIds.add(c.AccountId);
}
if (accountIds.isEmpty()) return;

Map<Id, Account> accounts = new Map<Id, Account>(
    [SELECT Id, Last_Contact_Added__c FROM Account WHERE Id IN :accountIds]
);
for (Account a : accounts.values()) {
    a.Last_Contact_Added__c = Date.today();
}
update accounts.values();

21.4 Other optimization techniques

Query only needed fields; use relationship subqueries instead of separate queries.

Use Map<Id, SObject> constructors and Sets to avoid nested loops (O(n²) CPU).

Cache results in static variables within a transaction (e.g., record types, custom metadata).

Avoid Schema.getGlobalDescribe() in loops; prefer Schema.SObjectType.Account tokens or Schema.describeSObjects(new List<String>{ 'Account' }).

Move heavy work to async Apex, which has higher limits.

Reduce heap: use SOQL for loops, clear large collections, avoid storing large Blob/String values.

Use Database.Stateful sparingly in batches — state is serialized between chunks.

21.5 Other org-wide limits to know

Async Apex executions per 24 hours: the greater of 250,000 or (number of user licenses × 200).

Apex code size: 6 MB of Apex characters per org (excluding managed-package code and @isTest classes); a single class is limited to 1 million characters.

Scheduled Apex: 100 scheduled jobs at a time.

Batch Apex: 5 batch jobs queued or active concurrently, plus up to 100 in the Apex flex queue (Holding status).

Long-running synchronous requests (> 5 s) are limited per org (10 concurrent); slow code can block other users.