---
title: "Salesforce governor limits, organised by what actually trips them"
url: "https://trailassembly.com/governor-limits-reference/"
topic: "Architecture"
published: "2026-09-09T07:01:41+00:00"
updated: "2026-10-06T13:51:08+00:00"
tested_on: "Summer ’26"
---

# Salesforce governor limits, organised by what actually trips them

**The one fact that explains most governor limit failures:** every limit below is counted *per transaction*, and a transaction includes every piece of code and configuration that runs during one save — your Apex, other people’s Apex, every managed package trigger, every flow, every [validation rule](https://trailassembly.com/validation-rule-vs-flow/), every recalculation they cause. Your class does not get 100 SOQL queries. The transaction gets 100, and your class is one of the things spending them.

Every limit in the tables below was read back from a Summer ’26 Developer org on 10 September 2026 using the `Limits` class — SOQL 100, query rows 50,000, QueryLocator rows 10,000, SOSL 20, DML 150, DML rows 10,000, CPU 10,000 ms, heap 6 MB, callouts 100, queueable jobs 50, sendEmail 10. One row was wrong and has been corrected: the platform reports 50 future calls in asynchronous contexts, not 0 — the restriction on chaining them is enforced at runtime rather than as a limit of zero.

## Start from the symptom, not the limit name

You are probably here because something failed. Find the message.

| What you saw | Limit involved | Where it usually comes from |
| --- | --- | --- |
| Too many SOQL queries: 101 | 100 SOQL queries (sync) | A query inside a loop, or stacked automation on one object |
| Apex CPU time limit exceeded | 10,000 ms CPU (sync) | Nested loops, formula-heavy recalculation, too many flows on one object |
| Too many DML rows: 10001 | 10,000 DML rows | Cascading updates across a related-record hierarchy |
| Too many query rows: 50001 | 50,000 rows retrieved | A query with no selective filter, or no LIMIT |
| Apex heap size too large | 6 MB heap (sync) | Holding whole query results in collections instead of streaming |
| Too many DML statements: 151 | 150 DML statements | An update inside a loop |
| Too many callouts: 101 | 100 callouts | One callout per record instead of one batched request |
| You have uncommitted work pending | Not a limit — an ordering rule | DML before a callout in the same transaction |
| Non-selective query against large object type | Query selectivity, not a counted limit | Filtering on an unindexed field on a big object |

The last two are worth separating out. Neither is a counter you are exhausting. Both are rules about how the request is shaped, and both are covered further down.

## The complete per-transaction table

These are the numbers from the Apex Developer Guide’s per-transaction limits. Where sync and async differ, the difference is the whole point — see *Synchronous vs asynchronous governor limits, side by side*.

| Limit | Synchronous | Asynchronous |
| --- | --- | --- |
| SOQL queries issued | 100 | 200 |
| Records retrieved by SOQL queries | 50,000 | 50,000 |
| Records retrieved by Database.getQueryLocator | 10,000 | 10,000 |
| SOSL queries issued | 20 | 20 |
| Records retrieved by a single SOSL query | 2,000 | 2,000 |
| DML statements issued | 150 | 150 |
| Records processed as a result of DML | 10,000 | 10,000 |
| CPU time | 10,000 ms | 60,000 ms |
| Heap size | 6 MB | 12 MB |
| Total callouts | 100 | 100 |
| Cumulative callout timeout | 120 s | 120 s |
| Total stack depth for recursive triggers | 16 | 16 |
| Methods with the @future annotation | 50 | 50 reported, but calling one from a future or batch context throws |
| Jobs added with System.enqueueJob | 50 | 1 |
| sendEmail method calls | 10 | 10 |
| EventBus.publish calls | 150 | 150 |
| Maximum execution time per transaction | 10 minutes | 10 minutes |
| Push notification method calls | 10 | 10 |

Two structural things about this table. The rows that are identical in both columns (DML statements, DML rows, query rows, callouts) are the ones you cannot escape by going asynchronous. The rows that differ are the ones where async buys you room. Knowing which is which changes how you fix a failure.

## “Per transaction” is doing more work than it looks

A transaction is one unit of work the platform commits or rolls back as a whole. Practically, that means one of these:

- One record save from the UI or the API, including every trigger, flow, workflow rule, and process it fires, plus roll-up recalculation and any second save cycle those cause

- One `@AuraEnabled` or Visualforce controller method invocation

- One `execute()` method of a batch job — each chunk is its own transaction with a fresh budget

- One `@future` or Queueable execution

- One anonymous Apex block

Three consequences that come up constantly.

**Declarative automation spends the same budget as Apex.** A record-triggered flow doing a Get Records inside a loop consumes SOQL queries from the same pool of 100 that your trigger is drawing on. Nothing marks flow consumption as separate. If your Apex was fine last quarter and fails now, check what admins added to the object.

**Managed packages consume it too, but not entirely from your pool.** Certified managed package code gets its own per-namespace allowance (the documented limit is 100 SOQL queries for each namespace) with a cumulative cross-namespace ceiling of 1,100 SOQL queries, 1,650 DML statements, 1,100 callouts, and 110,000 `getQueryLocator` records. So a package with a busy trigger is not usually eating your 100 queries directly. It *is* sharing CPU time and heap with you, which is not namespaced.

**Recursion is cumulative.** If a trigger causes a second save that fires the trigger again, both passes count against one budget. Three passes at 35 queries each fails, even though no single pass looks unreasonable. The recursive trigger stack depth limit of 16 exists to stop this going on forever, but you will exhaust SOQL or CPU long before you reach depth 16.

## The four limits people actually hit

In practice almost every governor failure in a real org is one of four things.

**SOQL 101.** The most common by a wide margin, and nearly always a query executing once per record. It is also the one most likely to be caused by something other than the code that reports the error — the failing class is just the one holding the 101st query. Covered in detail in *[Too many SOQL queries](https://trailassembly.com/soql-101/): 101*.

**CPU time.** The second most common and the hardest to attribute, because there is no line number in the exception and the cost is spread across everything that ran. This is the limit that fails intermittently, passes in sandbox, and fails in production, because it depends on how much other automation happens to be active. See *Apex [CPU time limit](https://trailassembly.com/cpu-time-limit/) exceeded*.

**DML rows: 10,000.** Usually a cascade. You update 200 accounts, each recalculating its contacts, each of those touching child records, and the total row count crosses 10,000 without any single statement looking large. The number that matters is rows processed across the whole transaction, not per statement.

**[Heap size](https://trailassembly.com/heap-size-too-large/).** Turns up when someone queries a large result set into a `List` and then builds Maps from it. 6 MB goes faster than people expect once you hold sObjects with many populated fields. The fix is nearly always to select fewer fields or to iterate with a SOQL for-loop, which processes records in chunks of 200 rather than materialising everything.

## The limits that are effectively theoretical

Being honest about this saves you optimising the wrong thing.

**150 DML statements** is generous. If you are approaching it you have a DML inside a loop, and the count is a symptom rather than the problem. Well-structured code uses a handful of statements regardless of volume.

**20 SOSL queries** — most orgs never issue one from Apex.

**10 minutes maximum execution time** is unreachable synchronously, because a UI transaction dies of other causes long before, and rare even in async. Treat it as a backstop, not a design constraint.

**16 recursive trigger stack depth** — you will hit SOQL or CPU limits at depth three or four. If you genuinely reach 16 you have a loop, not a limit problem.

**50,000 SOQL rows** sits in an awkward middle. It is unreachable in a well-filtered transaction and instant in a badly filtered one, so it functions less as a capacity limit and more as an alarm telling you a query has no useful `WHERE` clause.

**100 callouts** is rarely reached, because the cumulative 120-second callout timeout usually bites first. One hundred callouts averaging more than 1.2 seconds each cannot fit in a single transaction whatever the count says.

## CPU time excludes exactly the slow parts, which is why estimates are wrong

This is the most widely misunderstood item on the whole list, and worth stating precisely. The Apex Developer Guide says CPU time “is calculated for all executions on the Salesforce application servers occurring in one Apex transaction” and “for the executing Apex code, and for any processes that are called from this code, such as package code and workflows.”

So it counts: your Apex, managed package Apex, flows, [workflow rules](https://trailassembly.com/migrate-workflow-rules/), validation rules, formula evaluation. Anything computing on the application server.

It explicitly does *not* count: database execution time for SOQL, SOSL, and DML, or time spent waiting on an HTTP callout.

The practical effect is counterintuitive. A transaction that takes 40 seconds of wall-clock time can pass the 10,000 ms CPU limit comfortably if 35 of those seconds were spent in the database and on callouts. And a transaction that finishes in under two seconds of wall clock can fail, if those two seconds were nested loops over collections. Wall-clock timing tells you nothing useful here. Only `Limits.getCpuTime()` does.

The corollary that trips people: consolidating five queries into one helps your SOQL count and does almost nothing for CPU time, while replacing a nested loop with a Map lookup does the opposite. Measure before choosing.

## Selectivity is a limit even though it is not in the limits table

Query selectivity governs whether the query optimiser can use an index, and on large objects a [non-selective](https://trailassembly.com/selective-queries-at-scale/) query is rejected outright rather than merely being slow. The thresholds are arithmetic, not vibes: a filter qualifies as selective when it is on an indexed field and reduces the result to under 10% of records for the first million rows, then under 5% of rows beyond the first million, capped at an absolute ceiling of 333,000 records.

What that means at scale: on an object with 5 million records the threshold works out at 300,000 rows: 100,000 for the first million, plus 5% of the remaining 4 million. Past roughly 5.7 million records the calculation exceeds the ceiling, so 333,000 becomes the only number that matters, and no filter returning more rows than that will ever be considered selective however well indexed the field is.

Indexed by default: `Id`, `Name`, `OwnerId`, `CreatedDate`, `SystemModstamp`, `RecordTypeId`, master-detail and lookup relationship fields, and fields marked [External Id](https://trailassembly.com/upsert-with-external-ids/) or Unique. Everything else needs a custom index, which means a support request. Note that `!=`, `NOT`, leading-wildcard `LIKE`, and comparisons against `null` generally defeat the index regardless of the field.

## Measure instead of guessing

Two tools, and you should know both.

The `Limits` class reports current consumption and the ceiling for every counter, at any point in execution:

```apex
System.debug('SOQL: '  + Limits.getQueries()      + '/' + Limits.getLimitQueries());
System.debug('Rows: '  + Limits.getQueryRows()    + '/' + Limits.getLimitQueryRows());
System.debug('DML: '   + Limits.getDmlStatements()+ '/' + Limits.getLimitDmlStatements());
System.debug('CPU: '   + Limits.getCpuTime()      + '/' + Limits.getLimitCpuTime());
System.debug('Heap: '  + Limits.getHeapSize()     + '/' + Limits.getLimitHeapSize());
```

Put those either side of a suspect block and you get the cost of that block rather than of the transaction. This is the only reliable way to attribute CPU time.

For attributing consumption across code you do not own, the debug log’s cumulative limit usage section breaks the same counters down per namespace:

```apex
LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 0 out of 100
  Number of query rows: 0 out of 50000
  Number of DML statements: 0 out of 150
  Maximum CPU time: 0 out of 10000
  Maximum heap size: 0 out of 6000000
```

One block per namespace, so you can prove whether your code or an installed package spent the budget. That is the fastest way to end an argument about whose problem a failure is, and it is covered step by step in *How to read LIMIT_USAGE_FOR_NS and prove whose code spent your limits*.

## Which fix to reach for

Bulkify first, always. One query and one DML statement per object per transaction, regardless of record count, is the property you want, and the test that proves it inserts 200 records, not one.

Go asynchronous second, and only knowing what it buys. Async doubles SOQL to 200, raises CPU to 60,000 ms, and doubles heap to 12 MB. It changes nothing about DML statements, DML rows, query rows, or callouts. Code whose cost scales linearly with record count will fail again at a larger batch size. You have bought a factor of six on CPU, not an exemption.

Redesign third. Batch Apex is the genuine escape hatch, because governor limits reset for each `execute()` call. The documentation states that “each execution of a batch Apex job is considered a discrete transaction.” That is a different guarantee from “higher limits”, and it is the only one that scales indefinitely.

## Frequently asked

### Can Salesforce raise a per-transaction governor limit for my org?

No. Per-transaction limits are fixed and identical across editions. Some org-level and daily limits vary by edition or licence count: asynchronous executions per 24 hours, for example, is 250,000 or licences × 200, whichever is greater, but the numbers in the per-transaction table are not negotiable.

### Do my managed packages eat my 100 SOQL queries?

Not for the counted resources. Certified managed packages get their own per-namespace allowance, with a cumulative cross-namespace ceiling of 1,100 SOQL queries. CPU time and heap are shared, which is why a package can push you over the CPU limit without touching your query count at all.

### Why does the same code pass in a scratch org and fail in production?

Because the transaction is bigger in production. More triggers, more flows, more validation rules, more packages, and more child records to recalculate. Governor limits are a property of the whole transaction, so testing in a clean org tests the wrong transaction.

### Does a query that returns zero rows still count?

Yes. Every SOQL query counts as one against the 100, whatever it returns, and regardless of `LIMIT 1` or a filter that matches nothing.

### What resets between the chunks of a batch job?

Everything per-transaction. Each `execute()` gets a fresh 200 SOQL queries, 150 DML statements, 60,000 ms of CPU, and 12 MB of heap. What does not reset is the org’s daily asynchronous execution allowance, or the 5 concurrent batch job ceiling.

### Are Flow limits separate from Apex limits?

No. Flows have some of their own element-level limits, but the SOQL queries, DML statements, DML rows, and CPU time a flow consumes come from the same per-transaction budget as Apex. There is one pool.

---

*Reference: [Apex Governor Limits](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm) and [Using Batch Apex](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_batch_interface.htm), Salesforce Apex Developer Guide; [Limits Class](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_limits.htm), Apex Reference Guide; Salesforce Query & Search Optimization Cheat Sheet for the selectivity thresholds.*

---

Source: https://trailassembly.com/governor-limits-reference/
Trail Assembly — independent Salesforce write-ups.
