Fix "Apex CPU Time Limit Exceeded" in Salesforce
"Apex CPU time limit exceeded" means your transaction used more than 10 seconds of CPU (60 seconds for async). Database time doesn't count — the culprit is computation: loops over large collections, nested trigger/Flow recursion, or inefficient code in triggers and managed packages. Find the hotspot in the debug log, then bulkify, cache with maps, or move work async.
What counts toward CPU time — and what doesn't
| Counts toward CPU | Does NOT count |
|---|---|
| Apex statement execution (loops, string/JSON processing, math) | SOQL query execution time (database) |
| Trigger logic — yours and managed packages' | DML database time |
| Flow element execution, formulas, validation rules, workflow | HTTP callout wait time |
| Declarative automation triggered by your DML | Time spent waiting on locks |
This is why the error is confusing: the visible slow part of a transaction (a big query) is often free, while an innocent-looking loop is what burns the budget.
The usual suspects
- Loops over loops. Nested iteration over related records (O(n²) comparisons) is the single most common cause. 200 trigger records × 1,000 comparisons each is 200,000 iterations before you've done anything useful.
- Trigger recursion. An update trigger that performs updates re-fires automation. Two or three re-entries multiply all trigger CPU.
- Non-bulkified helper methods called once per record instead of once per batch.
- Record-triggered Flows stacking on triggers. Each Flow element costs CPU; a loop inside a Flow over hundreds of records adds up fast.
- Heavy string/JSON processing — serializing large payloads, regex over long text, building huge strings by concatenation.
- Managed package automation you can't see — but which absolutely bills to your transaction's CPU.
Finding the hotspot in the debug log
Every log line carries a nanosecond elapsed counter, e.g. (5348351). The manual method:
14:23:07.1 (120000000)|CODE_UNIT_STARTED|[EXTERNAL]|OpportunityTrigger on Opportunity trigger event AfterUpdate
14:23:09.8 (2830000000)|CODE_UNIT_FINISHED|OpportunityTrigger on Opportunity trigger event AfterUpdate
2,830,000,000 − 120,000,000 = 2.71 s wall time in that trigger. Do this for every CODE_UNIT pair, rank them, then drill into the worst one. Also check the summary at the end of the log:
LIMIT_USAGE_FOR_NS|(default)|
Maximum CPU time: 9840 out of 10000 ******* CLOSE TO LIMIT
Doing this by hand across a 50,000-line log is exactly the tedium a debug log analyzer removes. ForceLens's Performance tab ranks code units by time consumed, its Timeline shows where the transaction's time physically went, and the Limits tab shows CPU consumption against the 10 s budget — parsed from the same log, in one click.
Fixes that actually work
- Bulkify. Query once before the loop; process collections; never call a per-record helper in a loop. Replace nested loops with
Map<Id, SObject>lookups — this alone turns O(n²) into O(n). - Stop recursion. Use a static guard variable or, better, restructure so triggers don't update their own object unnecessarily.
- Move work async. Queueables, future methods, and batch Apex get a 60 s CPU budget and run outside the user's save. Anything not needed synchronously — roll-ups, integrations, enrichment — belongs there.
- Tune Flows. Consolidate multiple record-triggered Flows per object, avoid loops over large collections inside Flows, and push heavy logic to (bulkified) Apex where appropriate.
- Audit managed packages. If a package's automation dominates the log, check its settings for toggles, or scope which record changes invoke it.
- Cache expensive computation. Static variables survive for the transaction — compute once, reuse.
Frequently asked questions
What is the Apex CPU time limit in Salesforce?
10,000 ms (10 seconds) per synchronous transaction; 60,000 ms for asynchronous (batch, future, queueable). Exceeding it throws an uncatchable limit exception and rolls back the transaction. Full list: governor limits explained.
What does NOT count toward CPU time?
Database time: SOQL execution, DML database operations, and callout wait time are excluded. Computation — Apex statements, trigger and Flow logic, validation, managed package code — is what counts.
How do I find what's consuming CPU in a debug log?
Diff the elapsed-time counters between CODE_UNIT_STARTED/FINISHED pairs and check LIMIT_USAGE_FOR_NS at the end of the log — or let ForceLens's Performance and Timeline tabs compute the ranking automatically.
Can I catch the CPU limit exception in try/catch?
No. System.LimitException is uncatchable. Prevention — bulkification, recursion control, async offloading — is the only fix.
Find your CPU hotspot in seconds
Open the failing transaction's log in ForceLens and the Performance tab ranks every code unit by time consumed — free, local, no setup.