The System.LimitException: Apex CPU time limit exceeded error occurs when a Salesforce transaction consumes more processing time on the application server than the platform allows. Salesforce enforces this governor limit to prevent any single transaction from monopolizing shared, multi-tenant resources.
CPU time is the measure of time the server spends executing code and declarative automation.
Debug logs are the primary tool for a granular analysis of a transaction.
Setting Up Optimal Debug Logs
To capture the necessary detail without generating an excessively large log file:
Avoiding Log Truncation and Managing Limits
ApexLog object. You can select the rows for the largest logs and click the "Delete Rows" button to quickly free up the most space.SELECT Id, LogLength FROM ApexLog ORDER BY LogLength DESC
If your log is still being truncated even with optimized debug levels, your best strategy is to reduce the transaction's scope. For example, instead of running a batch update on 200 records, replicate the issue with only 10 records. This will generate a smaller, complete log that still reveals the performance bottleneck.
Manually Identifying CPU-Intensive Operations
After generating a log, open the raw file and search for these key entries:
CODE_UNIT_STARTED and CODE_UNIT_FINISHED: This appears at the start and end of every method, trigger, or Flow execution. To find out how long a specific method took to run, find its matching STARTED and FINISHED pair in the log. The difference in the timestamps between these two lines tells you the total duration of that method's execution....|CODE_UNIT_FINISHED|...|MyHelperClass.processAccounts|
...|CODE_UNIT_FINISHED|...|MyTriggerHandler.updateContacts|
... |LIMIT_USAGE_FOR_NS|... |CPU_TIME|8560|10000
CUMULATIVE_PROFILING|Apex|MyTriggerHandler.updateContacts:52|1|8321
The Developer Console provides a built-in, visual log analyzer that is useful for quick analysis.
Steps for Analysis
Limitations of the Developer Console
This free VS Code extension is arguably the most powerful tool for analyzing performance from debug logs.
Reference: Apex Log Analyzer on VS Code Marketplace
Steps for Analysis
Install the Apex Log Analyzer extension in Visual Studio Code.
In VS Code, open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P) and run SFDX: Get Apex Debug Logs.
Select and download the log you want to analyze.
With the log file open, run the command Apex Log Analyzer: Show Analysis.
A new panel will open. Click on the Call Tree tab.
Key Features for CPU Analysis
Call Tree: This is the most valuable feature. It provides a hierarchical view of the entire transaction. You can expand the methods with the highest Total Duration to follow the "path of pain" down the execution stack until you find the method with a high Self Duration. This method is the root cause of the CPU consumption.
Timeline View: Offers a color-coded visualization of the transaction, making it easy to spot patterns like hundreds of repeated, small database calls.
Limitations
The tool is dependent on the quality of the debug log. If the log is truncated, the analysis will be incomplete.
When a log is consistently truncated, you can't see the final CPU usage. The Limits class allows you to proactively log CPU consumption at various checkpoints within your code.
How to Implement It
Place System.debug() statements at key points in your transaction to output the current CPU time.
Apex
public class MyComplexProcessor {
public void execute() {
System.debug(LoggingLevel.WARN, '--- START of process. CPU Used: ' + Limits.getCpuTime() + 'ms');
// First major operation
this.processInputs();
System.debug(LoggingLevel.WARN, '--- After processing inputs. CPU Used: ' + Limits.getCpuTime() + 'ms');
// Second, potentially slow operation
this.updateRelatedRecords();
System.debug(LoggingLevel.WARN, '--- After updating records. CPU Used: ' + Limits.getCpuTime() + 'ms');
}
// ... other methods
}
Benefits and Best Practices
Benefit: It provides visibility into CPU usage even if the transaction fails and the log is truncated. You can see the last checkpoint that was successfully passed.
When to Use: Use this when dealing with extremely large transactions (e.g., complex batch jobs) where logs are consistently truncated.
Best Practice: Use a distinct LoggingLevel like WARN or ERROR. This allows you to set your debug log's Apex Code level to WARN to get a very small log containing only your custom checkpoint messages, making analysis trivial.
Limitations
It requires modifying your Apex code.
It only tells you the cumulative CPU time at a point; it doesn't break down the cause like a full debug log.
Context: An OpportunityLineItem trigger needs to validate that the product's Family is not on a "disallowed" list stored on the parent Account's Country__c record, which is a lookup to a custom Country_Setting__c object.
Inefficient Code: The code iterates through each line item and performs a separate SOQL query for each one to get the parent details.
// Inefficient Trigger Handler
public class OliTriggerHandler {
public static void onBeforeInsert(List<OpportunityLineItem> newOlis) {
for (OpportunityLineItem oli : newOlis) {
// SOQL query INSIDE the loop
Opportunity opp = [SELECT Account.Country__r.Disallowed_Product_Families__c
FROM Opportunity
WHERE Id = :oli.OpportunityId];
String disallowed = opp.Account.Country__r.Disallowed_Product_Families__c;
Product2 prod = [SELECT Family FROM Product2 WHERE Id = :oli.Product2Id]; // Second SOQL in loop
if (disallowed != null && disallowed.contains(prod.Family)) {
oli.addError('This product family is not allowed for the account country.');
}
}
}
}
Why it failed: With 200 line items, this would execute 400 SOQL queries, but more importantly, the repeated database calls and loop processing burned CPU time preparing and handling each query.
Optimized Code: The code is bulkified to use maps and single queries.
// Optimized Trigger Handler
public class OliTriggerHandler {
public static void onBeforeInsert(List<OpportunityLineItem> newOlis) {
// Step 1: Collect all unique IDs needed
Set<Id> oppIds = new Set<Id>();
Set<Id> productIds = new Set<Id>();
for (OpportunityLineItem oli : newOlis) {
oppIds.add(oli.OpportunityId);
productIds.add(oli.Product2Id);
}
// Step 2: Query all data once using maps
Map<Id, Product2> productMap = new Map<Id, Product2>([SELECT Family FROM Product2 WHERE Id IN :productIds]);
Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([
SELECT Account.Country__r.Disallowed_Product_Families__c
FROM Opportunity
WHERE Id IN :oppIds
]);
// Step 3: Process with fast, in-memory lookups
for (OpportunityLineItem oli : newOlis) {
Opportunity opp = oppMap.get(oli.OpportunityId);
Product2 prod = productMap.get(oli.Product2Id);
String disallowed = opp.Account.Country__r.Disallowed_Product_Families__c;
if (disallowed != null && prod != null && disallowed.contains(prod.Family)) {
oli.addError('This product family is not allowed for the account country.');
}
}
}
}
Context: A batch job processes all Case records. The finish method must send a summary email that includes a count of cases processed for each RecordType.
Inefficient Code: The finish method re-queries all the cases that were just processed to perform its aggregation.
global class ProcessCasesBatch implements Database.Batchable<sObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([SELECT RecordTypeId FROM Case]);
}
global void execute(Database.BatchableContext bc, List<Case> scope) {
// Processing logic for each case...
}
global void finish(Database.BatchableContext bc) {
// Re-queries ALL cases again. Very slow and CPU intensive for large volumes.
Map<Id, Integer> counts = new Map<Id, Integer>();
for (Case c : [SELECT RecordTypeId FROM Case WHERE Status = 'Processed']) {
Integer currentCount = counts.get(c.RecordTypeId);
if (currentCount == null) currentCount = 0;
counts.put(c.RecordTypeId, currentCount + 1);
}
// Send email with counts...
}
}
Why it failed: The finish method's SOQL query could return millions of records, and iterating over them to build the map is extremely CPU-intensive. The transaction times out.
Optimized Code: Use Database.Stateful to aggregate results incrementally across execute batches.
global class ProcessCasesBatch implements Database.Batchable<sObject>, Database.Stateful {
// Stateful property to hold counts across batches
global Map<Id, Integer> countsByRecordType;
global ProcessCasesBatch() {
countsByRecordType = new Map<Id, Integer>();
}
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([SELECT RecordTypeId FROM Case]);
}
global void execute(Database.BatchableContext bc, List<Case> scope) {
// Processing logic...
// Aggregate counts within each execute method
for (Case c : scope) {
Integer currentCount = countsByRecordType.get(c.RecordTypeId);
if (currentCount == null) currentCount = 0;
countsByRecordType.put(c.RecordTypeId, currentCount + 1);
}
}
global void finish(Database.BatchableContext bc) {
// The map is already built. No SOQL needed.
// Send email with countsByRecordType...
System.debug('Final counts: ' + countsByRecordType);
}
}
Context: A record-triggered Flow on Account runs when a custom Tier__c field changes. The Flow must loop through all related Contact records and call an Apex Action to determine if each Contact is eligible for a new marketing campaign.
Inefficient Flow :
Start: Record-triggered Flow on Account update.
Get Records: Gets all related Contact records.
Loop: Iterates through the collection of Contacts.
Action (Inside Loop): Calls an Apex Action named Check Eligibility, passing the current Contact from the loop.
Why it failed: Calling an Apex Action has significant CPU overhead. If an Account has 150 contacts, the Flow invokes the Apex Action 150 times. The cumulative CPU cost of initializing the Apex and performing the check repeatedly causes a timeout, especially during a mass update of Accounts.
Optimized Flow & Apex: The Apex Action is modified to be bulk-safe, and the Flow is redesigned to call it only once.
Bulkified Apex Action: The method is changed to accept a List of Contact IDs and return a Map of results.
public class MarketingEligibilityCheck {
@InvocableMethod(label='Check Contact Eligibility (Bulk)')
public static List<Result> checkContacts(List<Request> requests) {
Set<Id> contactIds = new Set<Id>();
for(Request req : requests) {
contactIds.add(req.contactId);
}
// Complex logic now runs on the entire set at once
Map<Id, Contact> contactsWithData = new Map<Id, Contact>([
SELECT Id, DoNotCall, HasOptedOutOfEmail FROM Contact WHERE Id IN :contactIds
]);
List<Result> results = new List<Result>();
for (Request req : requests) {
Contact c = contactsWithData.get(req.contactId);
Result res = new Result();
res.contactId = c.Id;
res.isEligible = (!c.DoNotCall && !c.HasOptedOutOfEmail); // Example logic
results.add(res);
}
return results;
}
public class Request {
@InvocableVariable(required=true)
public Id contactId;
}
public class Result {
@InvocableVariable
public Id contactId;
@InvocableVariable
public Boolean isEligible;
}
}
Optimized Flow Design:
Start: Same.
Get Records: Same.
Loop: Iterates through the Contact collection.
Assignment (Inside Loop): Adds the Id of the current Contact to a collection variable (e.g., contactIdCollection).
Action (After Loop): Calls the bulkified Check Eligibility Apex Action once, passing the entire contactIdCollection.
The Flow can then loop through the results from the action if further logic is needed.
005132111

We use three kinds of cookies on our websites: required, functional, and advertising. You can choose whether functional and advertising cookies apply. Click on the different cookie categories to find out more about each category and to change the default settings.
Privacy Statement
Required cookies are necessary for basic website functionality. Some examples include: session cookies needed to transmit the website, authentication cookies, and security cookies.
Functional cookies enhance functions, performance, and services on the website. Some examples include: cookies used to analyze site traffic, cookies used for market research, and cookies used to display advertising that is not directed to a particular individual.
Advertising cookies track activity across websites in order to understand a viewer’s interests, and direct them specific marketing. Some examples include: cookies used for remarketing, or interest-based advertising.