Fix Guides

How to Fix "Apex CPU Time Limit Exceeded" in Salesforce

Step-by-step fix guide with AI-powered diagnosis from BuildForce.

CPU time is capped at 10,000ms (10s) for synchronous Apex and 60,000ms (60s) for async (Queueable, Batch, Future) — and it only counts actual compute, not time spent waiting on the database or a callout. So this is almost never "too much data" — it's an inefficient algorithm: a nested loop, a regex compiled inside a loop, or heavy string concatenation. Fix: find the hot method in the debug log, replace O(n²) patterns with Map/Set lookups, and if the work is genuinely heavy, move it to async where the ceiling is six times higher.

CPU Time vs. Total Execution Time

These are different measurements, and confusing them wastes debugging time. Total execution time includes everything — Apex compute, SOQL query wait, DML wait, callout wait. CPU time excludes all the waiting and measures only the processor time your code itself consumes. If a transaction takes 8 seconds total but the debug log shows only 500ms of CPU time, the bottleneck is the database or network round-trips — not the Apex logic — and bulkifying loops won't fix it.

Common Causes

  • Nested loops (O(n²)) — most often a List.contains() call inside a loop over another collection.
  • Regex recompiled inside a loop — Pattern.compile() should run once, not per record.
  • String concatenation with += inside a loop — allocates a new String object every iteration.
  • Recursive trigger chains — recomputing the same calculation multiple times in a single transaction.
  • Repeated JSON serialize/deserialize of large payloads that could be computed once and reused.
  • Heavy business logic re-executed per record instead of computed once and cached for the transaction.

How to Fix It — Step by Step

  1. 1. Pull the debug log for the failing transaction. Setup → Debug Logs (or enable a trace flag for the affected user) and reproduce the failure. Look at the log's timestamps and the CUMULATIVE LIMIT USAGE section to find which method block is actually slow — don't guess from the trigger name alone.
  2. 2. Find O(n²) patterns first. Any List.contains() call, or any lookup, inside a loop that iterates another collection is an O(n²) pattern. Replace the List with a Set<Id> (or Map<Id, SObject>) built once before the loop — Set/Map lookups are O(1) instead of an O(n) linear scan repeated n times.
  3. 3. Move Pattern.compile() outside the loop. Regex compilation is expensive. If a Pattern is being compiled inside a for-loop, compile it once above the loop and reuse the Matcher inside.
  4. 4. Replace string concatenation in loops. String += inside a loop allocates a new String object every iteration. Build a List<String> and call String.join() once after the loop instead.
  5. 5. Add a recursion guard. Use a static Set<Id> (or Boolean) in a helper class to track which record Ids have already been processed in the current transaction, so a trigger chain doesn't recompute the same expensive logic multiple times per transaction.
  6. 6. Move genuinely heavy work to async. If the computation is necessary and CPU-intensive at real scale (bulk scoring, large JSON processing), move it to Queueable or Batch Apex — the ceiling there is 60,000ms, six times the synchronous limit.
  7. 7. Re-test with realistic bulk volume. Run the fix against 200+ records, not 1. CPU-time bugs are invisible at small scale and only appear once the algorithm's complexity actually matters — test at the volume that will hit production.

Example: Eliminating a Nested Loop

// BAD — O(n^2): List.contains() re-scans the whole list every iteration
for (Opportunity opp : opportunities) {
    if (closedAccountIds.contains(opp.AccountId)) { // O(n) scan, n times
        // ...
    }
}

// GOOD — O(n): Set lookup is O(1)
Set<Id> closedAccountIdSet = new Set<Id>(closedAccountIds);
for (Opportunity opp : opportunities) {
    if (closedAccountIdSet.contains(opp.AccountId)) {
        // ...
    }
}

This Error Is a Symptom, Not the Root Cause

A nested-loop or regex-in-loop pattern that shipped in one class is a style the same developer (or the same copied boilerplate) likely used elsewhere. Run BuildForce's free health check to see how your org's overall setup compares to similar companies, then connect your org so BuildForce can scan every Apex class for CPU-risk patterns instead of waiting for the next bulk import to find the next one.

How BuildForce Prevents This

FAQ

What's the actual CPU time limit — 10 seconds or 10,000ms?

They're the same number: 10,000ms (10 seconds) for synchronous Apex, and 60,000ms (60 seconds) for asynchronous Apex (Future, Queueable, Batch execute()). The limit is per-transaction, and it's shared across every trigger, class, and flow-invoked Apex action that runs within that single transaction.

Does a slow SOQL query count against CPU time?

No — this is the most common misunderstanding. CPU time measures actual processor compute in your Apex code. Time spent waiting on a SOQL query, a DML statement, or an HTTP callout to return does NOT count against CPU time (though it does count against total execution time and other limits). If your CPU time is low but the transaction is still slow, the bottleneck is the database or network, not your Apex logic — bulkifying loops won't fix that.

Why does this only happen with bulk data, not in my unit test with 1 record?

An O(n²) algorithm — a nested loop or a List.contains() call inside a loop — is invisible with 1 or even 20 records because the total operation count is trivially small. At 200 records, an O(n²) pattern does 40,000 operations instead of 200; at 2,000 records, it's 4,000,000. Always test with realistic bulk volumes (200+ records is the Salesforce standard) to catch this class of bug before production.

Does async Apex really get 6x the CPU budget?

Yes — 60,000ms vs 10,000ms. But that's not a substitute for fixing an inefficient algorithm; it just moves the wall further away. Use async for work that's genuinely CPU-heavy at scale (bulk scoring, large-payload processing), not to paper over a nested loop that should be a map lookup.

Can BuildForce catch inefficient Apex before it ships?

Yes. BuildForce's Apex health checks flag common CPU-time risk patterns — nested loops over large collections, List.contains() inside a loop, and regex compilation inside a loop — before the code reaches production, instead of waiting for a bulk data load to trip the limit.