SfSkills Domains Docs GitHub

Home / apex / apex-cpu-and-heap-optimization

apex-cpu-and-heap-optimization

Use when diagnosing or preventing Apex CPU time and heap size problems, including nested-loop refactors, JSON memory pressure, string work, and `Limits.getCpuTime()` checkpoints. Triggers: 'CPU time limit exceeded', 'heap size too large', 'string concatenation', 'regex in loop', 'Limits.getCpuTime', 'System.LimitException', 'Apex heap size too large', 'SOQL for loop', 'Database.Stateful heap', 'batch job fails on later chunks'. NOT for generic SOQL/DML governor-limit basics — use apex/governor-limits. NOT for runtime limit guard clauses — use apex/apex-limits-monitoring.

Open the skill folder on GitHub


Use this skill when Apex is already bulkified but still failing because the transaction does too much compute or holds too much memory. CPU and heap problems are often caused by algorithm shape, payload handling, or repeated expensive work rather than by the usual SOQL/DML mistakes alone.

standards/decision-trees/performance-tuning.md routes here from Q2 → Q3 ("CPU time exceeded" or "Heap size exceeded", then nested loops / JSON / string work). If the symptom is a count limit — too many SOQL queries, DML rows, callouts — that tree's Q4 sends you to apex/governor-limits instead. Its Rule 0 applies first: without a measurement you are guessing, so start at apex/apex-performance-profiling when the hotspot is unknown.

Before Starting

Questions to Ask Before Configuring

Ask these before changing a line; each one maps to a gotcha in references/gotchas.md that has already cost somebody a production incident.

AskWhy it mattersWhat a good answer adds
"Which limit failed, in which context — sync, batch/future, or scheduled?"The ceiling differs 6x between the columns, and scheduled Apex sits in the synchronous one despite being asyncThe real budget, so the target is a number rather than "faster"
"Do you have a measurement, or an exception message?"An exception names the limit that broke first, not the largest consumer; the debug log's LIMIT_USAGE_FOR_NS block names all of themA hotspot instead of a guess, and a baseline to prove the fix
"What is the record volume the failing transaction actually saw?"CPU and heap defects are invisible at one record; the reported heap can even read 0 on a small runThe volume the bulk test must reproduce
"Does any managed package or other trigger run in the same save order?"Certified packages share your CPU and heap even though they get their own SOQL and DMLWhether the fix is in your class at all, or is "move this async"
"Is any of this work retained across chunks or invocations — Database.Stateful, a static cache, a chained queueable?"Governor counters reset per chunk while retained state does not, which is why batches fail late rather than earlyThe accumulator that has to be bounded
"What must still be true after the refactor — ordering, partial success, error reporting?"Limit exceptions are uncatchable, so any "log the failure" design silently disappearsThe error path that has to move before the expensive block
"Who consumes this transaction afterwards — a callout, a flow, another trigger?"A fix that lands at 95% of the ceiling has no headroom for the next featureThe headroom fraction the test should assert

What a proper optimization adds over just making it faster: a measured before/after in the same units the platform enforces, a bulk test that fails the build when the headroom is spent again, and an explicit record of which ceiling was traded for which.

Core Concepts

The budget, exactly

ResourceSyncAsync (batch, future)Runtime accessor
Maximum CPU time on Salesforce servers10,000 ms60,000 msLimits.getCpuTime() / getLimitCpuTime()
Total heap size6 MB12 MBLimits.getHeapSize() / getLimitHeapSize()
Maximum execution time per Apex transaction10 minutes10 minutes—

Source: Salesforce Developer Limits and Allocations Quick Reference, Per-Transaction Apex Limits (salesforce_app_limits_cheatsheet.txt L89–L94), mirrored at apexdev.txt L19577–L19601. Two qualifiers that change how the table is used: scheduled Apex takes the synchronous column, and CPU and heap are the two ceilings a certified managed package does not get its own copy of. Both are worked through in references/gotchas.md.

CPU And Heap Fail For Different Reasons

CPU time is usually consumed by algorithmic cost: nested loops, repeated parsing, regex, sorting, or complex branching. Heap is usually consumed by retained data volume: large lists, maps, payloads, or serialized strings. A fix for one does not necessarily help the other — and the platform's own heap remedy, the SOQL for loop, explicitly costs CPU.

What Counts Toward CPU

The quick reference (salesforce_app_limits_cheatsheet.txt L170–L177) draws the line: application-server CPU spent in DML is counted; the portion of execution time spent in the database for DML, SOQL and SOSL is not, and neither is callout wait time. So a transaction can be slow for minutes with a small getCpuTime() reading, and a fast-looking bulk update can be the thing that exhausts it.

Algorithm Shape Beats Micro-Optimizing Syntax

Replacing nested loops with Map<Id, SObject> lookups or precomputed sets usually matters more than shaving tiny operations. The biggest gains often come from changing data structures and reducing repeated work. references/code-examples.md §1–§2 is the before/after pair.

Large Payload Handling Is A Memory Problem First

JSON responses, long string concatenation, and large debug serialization can explode heap quickly. Chunking work, nulling references when no longer needed, and avoiding redundant copies often matter more than clever loops. Apex String size is itself bounded by the heap limit (apexdev.txt L1275), and a collection has no item-count limit — only the heap bound (apexdev.txt L1431).

Measure Before And After

Limits.getCpuTime() and lightweight checkpoints help identify where the transaction burns time. Bracket blocks, never iterations. Limits.getHeapSize() is documented as approximate (apexrefguide.txt L220711–L220722), so read it as headroom rather than accounting.

Common Patterns

Map/Set Refactor For Nested Loops

When to use: CPU time is being spent in N x M record comparisons.

How it works: Build maps or sets once, then use constant-time lookups inside the loop.

Why not the alternative: Micro-tuning the loop body does little if the algorithm is still quadratic.

Chunk Large Payload Work

When to use: Heap spikes during JSON parsing, serialization, or large-string processing.

How it works: Process smaller batches, avoid duplicate payload copies, and release references after use. Inside one transaction the lever is the SOQL for loop, which iterates in batches of 200 (apexdev.txt L20271–L20285); across transactions it is Batch Apex, where limits reset for each execute (apexdev.txt L17709–L17710).

Instrument With Lightweight Checkpoints

When to use: The hotspot is unclear.

How it works: Add Limits.getCpuTime() and Limits.getHeapSize() deltas around suspect blocks and report them through templates/apex/ApplicationLogger.cls so the measurement survives in production, where debug logs are usually off.

Gate The Test On Headroom, Not On Green

When to use: Every time you land a CPU or heap fix.

How it works: A bulk test at 200+ records captures the deltas inside Test.startTest()/Test.stopTest() and asserts them against Limits.getLimit*() as a fraction. See references/code-examples.md §4.

Decision Guidance

SituationRecommended ApproachReason
CPU time dominated by nested comparisonsRefactor with maps/setsAlgorithm change gives the biggest gain
Heap limit triggered by large payloads or stringsReduce retained objects and process in chunksMemory pressure is the actual bottleneck
Heap fixed with a SOQL for loop, now CPU failsMove the chunking to Batch ApexEach execute gets a fresh set of limits; a for loop does not
Hotspot is unclearAdd temporary CPU checkpointsMeasure before refactoring blindly
Your own delta is small but the transaction still failsLook at the rest of the save order, then move work asyncCPU and heap are shared with certified managed packages
Batch succeeds early and fails on a late chunkSize-check the Database.Stateful accumulatorRetained instance state grows while counters reset
Work is intrinsically heavy for one transactionMove or split work into async chunksSometimes the right fix is architectural
  1. Establish the ceiling and the baseline. Confirm the context (sync / batch / future / scheduled), read the applicable row from the budget table above, and capture a LIMIT_USAGE_FOR_NS block from a failing run — references/examples.md Example 3 has the command.
  2. Localise the cost. Bracket suspect blocks with Limits.getCpuTime() / getHeapSize() deltas logged through templates/apex/ApplicationLogger.cls; never probe per iteration. If the hotspot is still unclear, stop here and use apex/apex-performance-profiling.
  3. Pick the lever from the shape, not the symptom. Nested loops → Map indexing; retained query results → SOQL for loop or Batch Apex; payload copies → parse once, typed classes, no defensive clones. standards/decision-trees/performance-tuning.md Q3 lists the branch for each.
  4. Refactor against the canonical artifact. Follow references/code-examples.md §2 (Map-indexed service), §3 (heap-safe batch), reusing templates/apex/BaseService.cls, templates/apex/BaseSelector.cls and templates/apex/ApplicationLogger.cls rather than inventing equivalents.
  5. Prove it at 200+ records. Write or extend the bulk test from §4 with headroom assertions taken inside Test.startTest()/Test.stopTest(), seeded from templates/apex/tests/TestDataFactory.cls.
  6. Run the static check, then the org check. python3 scripts/check_apex_cpu_and_heap_optimization.py --manifest-dir <classes dir>, then sf apex run test --tests <TestClass> --result-format human, then re-read the limit-usage block and compare it against the baseline from step 1.
  7. Record the trade. State which ceiling you spent to buy the other one, and what headroom is left, in the change description — the next person's fix depends on it.

Review Checklist

Salesforce-Specific Gotchas

  1. CPU and heap limits can come from different root causes in the same transaction — treat them separately.
  2. Large debug or JSON serialization can become the problem — diagnostics can make the incident worse.
  3. Nested loops over related record sets are classic CPU traps even after SOQL is bulkified — the database is no longer the bottleneck.
  4. Nulling references helps only after the data is no longer needed — memory discipline must match object lifetime.
  5. Certified managed packages share your CPU and heap — they get their own SOQL and DML, not their own compute budget.
  6. Scheduled Apex gets the synchronous ceiling — 10,000 ms, not 60,000 ms.
  7. System.LimitException is uncatchable and skips finally — the log line you were counting on never runs.
  8. Maximum heap size: 0 in a debug log means unmeasured, not clean.

Full treatment, with sources, in references/gotchas.md.

Output Artifacts

ArtifactDescription
CPU/heap reviewFindings on hot algorithms, payload pressure, and bad memory habits
Optimization planOrdered remediations targeting the highest-cost patterns first
Refactored class + -meta.xmlMap-indexed service or heap-safe batch, per references/code-examples.md
Bulk test with headroom assertions200+ record test that fails when the budget is spent again
Checkpoint strategyTemporary instrumentation guidance for locating hotspots
Before/after limit-usage readingsThe evidence that the trade actually paid

Reference Files

FileRead it when
references/code-examples.mdYou need the deployable artifact: before/after service, heap-safe batch, bulk test with headroom assertions, -meta.xml, package.xml, deploy and verify commands
references/gotchas.mdA limit behaves differently from what the table implies — managed packages, scheduled Apex, Database.Stateful, uncatchable exceptions, log heap of 0
references/examples.mdYou want a short worked pattern: Map refactor, checkpointing, reading the limit-usage block out of a debug log
references/llm-anti-patterns.mdYou are reviewing AI-generated optimization advice, or writing some
references/well-architected.mdYou are framing the finding for an architecture review, or need the source list with the claim each one supports

Package files