---
title: "System.LimitException: Apex CPU time limit exceeded"
url: "https://trailassembly.com/cpu-time-limit/"
topic: "Apex and Development"
published: "2026-09-09T04:08:29+00:00"
updated: "2026-10-08T15:50:03+00:00"
error_codes: "LimitException"
---

# System.LimitException: Apex CPU time limit exceeded

**The short version:** something in your transaction is doing per-record work that should be set-based, and the usual culprit is a nested loop. You get 10,000 ms of CPU time in a synchronous transaction and 60,000 ms asynchronously. Unlike query limits, this one is not solved by counting — it is solved by finding the code whose cost grows faster than the record count.

## What the clock is actually measuring

CPU time measures compute on the application servers. Understanding what it excludes is what makes this error tractable:

- **Counted:** Apex execution, loops, collection and string manipulation, JSON serialisation, validation rules, flow and process logic, assignment rules, and managed package code that runs in your transaction.

- **Not counted:** time the database spends executing your SOQL and DML, and time spent waiting on an HTTP callout.

That second list is the useful part. If you are hitting the CPU limit, adding more queries is not what did it, and the fix is not fewer queries. The fix is less computation between them. Teams routinely spend a day optimising SOQL and see the CPU number barely move, because database time was never on the clock.

## Cause 1: the nested loop

This is the overwhelming majority of cases. Two loops over collections of the same rough size means the work grows with the square of the record count. At 20 records nobody notices. At 200 it is 40,000 iterations, and at 2,000 it is four million.

```apex
// Cost grows as accounts × contacts.
for (Account a : accounts) {
    for (Contact c : contacts) {
        if (c.AccountId == a.Id) {
            // ...
        }
    }
}
```

Build a Map once and the inner loop disappears:

```apex
Map<Id, List<Contact>> byAccount = new Map<Id, List<Contact>>();
for (Contact c : contacts) {
    if (!byAccount.containsKey(c.AccountId)) {
        byAccount.put(c.AccountId, new List<Contact>());
    }
    byAccount.get(c.AccountId).add(c);
}

for (Account a : accounts) {
    List<Contact> related = byAccount.get(a.Id);
    if (related == null) { continue; }
    // ...
}
```

Two passes instead of a multiplication. This single change is the fix in most CPU-limit incidents, and it is worth checking for before investigating anything else.

## Cause 2: invariant work performed inside a loop

Anything whose result does not change per iteration should be computed once above the loop. Describe calls are the classic offender because they look free:

```apex
// Recomputed on every iteration.
for (SObject record : records) {
    Map<String, Schema.SObjectField> fields =
        Schema.getGlobalDescribe().get('Account').getDescribe().getFields().getMap();
    // ...
}
```

`getGlobalDescribe()` is expensive. Hoist it out, or cache it in a static. The same applies to building the same Set repeatedly, re-sorting a list that has not changed, and string concatenation in a loop — for large volumes, accumulate into a `List<String>` and join once at the end rather than appending to a growing string.

## Cause 3: too much automation on one object

Every flow, [validation rule](https://trailassembly.com/validation-rule-vs-flow/), and trigger on the object shares the same 10,000 ms. An object that accumulated automation over five years can exhaust the limit with no single component doing anything unreasonable.

This one is diagnosis before code. Set the Apex Code log level to FINEST, reproduce, and read the cumulative profiling section at the end of the log. It attributes time and lets you rank components by cost rather than by suspicion. Frequently the answer is that four flows on the object should be one, or that a [validation rule](https://trailassembly.com/validation-rule-vs-flow/) with a [cross-object formula](https://trailassembly.com/cross-object-formulas/) is being evaluated far more often than anyone intended.

## Cause 4: recursion

As with query limits, the CPU clock is cumulative across the whole transaction. Three passes of automation at 4,000 ms each fails, even though no individual pass looks expensive. If the log shows the same trigger or flow starting more than once, fix the recursion before optimising anything. Otherwise you are optimising work that should not be running at all.

## Measuring it properly

```apex
Long start = Limits.getCpuTime();
// work under investigation
System.debug('CPU used: ' + (Limits.getCpuTime() - start) +
             ' of ' + Limits.getLimitCpuTime());
```

Bracket sections rather than checking once at the end. A single total tells you that you have a problem. Deltas tell you where it is.

Then test at volume, and test the shape of the growth. If 50 records cost 300 ms and 200 records cost 4,800 ms, cost is scaling roughly with the square of the input and you have a nested loop somewhere, whether or not you can see it yet. Linear growth means the per-record work is simply too expensive, which is a different fix.

## When the honest answer is asynchronous

Sometimes the work genuinely requires more than 10,000 ms. Moving to [Queueable](https://trailassembly.com/apex-choosing-async/) or [Batch Apex](https://trailassembly.com/apex-batch-that-scales/) raises the ceiling to 60,000 ms and lets each batch execution start with a fresh allocation.

Be clear-eyed about the trade you are making: the work no longer happens in the user’s save, so the record is briefly in a state where your logic has not run yet. That is fine for recalculating a rollup and wrong for enforcing a validation. Async is a legitimate architectural choice, not a way to avoid fixing an algorithm, and if your code is quadratic, a larger budget only postpones the failure to a larger batch size.

## Frequently asked

### Does SOQL execution time count toward CPU time?

No. Database execution time and callout wait time are both excluded. Only application-server compute counts.

### Do managed packages consume my CPU budget?

Yes. Everything running in the transaction shares one allocation, whoever wrote it.

### Can the limit be raised?

No. Per-transaction limits are fixed and not adjustable by support.

### Why does it pass in the sandbox and fail in production?

Usually data shape rather than data volume. A production account with 8,000 contacts against a sandbox account with twelve. Nested loops are invisible until one side of the multiplication gets large.

### Do flows count separately from Apex?

No. One shared budget per transaction across all automation.

---

*Reference: [Per-Transaction Apex Governor Limits](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm), Salesforce Apex Developer Guide.*

---

Source: https://trailassembly.com/cpu-time-limit/
Trail Assembly — independent Salesforce write-ups.
