---
title: "System.LimitException: Too many query rows: 50001"
url: "https://trailassembly.com/query-rows-50001/"
topic: "Apex and Development"
published: "2026-09-09T05:44:43+00:00"
updated: "2026-10-06T13:51:03+00:00"
error_codes: "LimitException, QueryException"
---

# System.LimitException: Too many query rows: 50001

**The short version:** your transaction has pulled back more than 50,000 records across all of its SOQL queries combined. Add a selective `WHERE` clause, replace “fetch everything and count it” with an aggregate query, or move the work into [Batch Apex](https://trailassembly.com/apex-batch-that-scales/), where a `Database.QueryLocator` in the `start` method streams past this limit entirely. A SOQL for loop will *not* save you — it protects heap, not row count.

## The limit is cumulative and identical in async

50,000 is the total number of records returned to your transaction by every query it issues, added together. Ten queries of 6,000 rows each fails just as surely as one query of 60,000.

| Limit | Synchronous | Asynchronous |
| --- | --- | --- |
| Records retrieved by SOQL queries | 50,000 | 50,000 |
| SOQL queries issued | 100 | 200 |
| Records retrieved by Database.getQueryLocator | 10,000 | 10,000 |
| Records processed by DML | 10,000 | 10,000 |
| Total heap size | 6 MB | 12 MB |

Going asynchronous doubles your query *count* to 200 and doubles heap to 12 MB, but the row limit stays at 50,000. This trips up people who move a failing report generator into a `@future` method and watch it fail identically.

## Cause 1: a query with no bound on the data volume

The query was written against a sandbox with 3,000 records and shipped to an org with 400,000.

```apex
// Fine in the sandbox. Fatal in production.
List<Task> allTasks = [SELECT Id, Subject, WhatId FROM Task WHERE IsClosed = false];
```

Two questions decide the fix. Does the code need the field values, or only how many there are? And does it need all of history, or a window?

```apex
// If you only need a count, retrieve one row instead of 400,000.
Integer openTasks = [
    SELECT COUNT() FROM Task WHERE IsClosed = false
];

// If you need the records, bound them.
List<Task> recent = [
    SELECT Id, Subject, WhatId FROM Task
    WHERE IsClosed = false AND CreatedDate = LAST_N_DAYS:30
    LIMIT 5000
];
```

`COUNT()` is the cheapest change available here. An aggregate query returns one row regardless of how many records it counted, so a limit failure becomes a limit non-event. The catch is that aggregate queries have their own separate limit on how many of them you can issue per transaction, so `COUNT()` in a loop reintroduces the problem in a new form.

Add `LIMIT` deliberately, not defensively. A `LIMIT 5000` on a query that legitimately has 8,000 matching records does not fix a bug, it hides one — the code now silently produces wrong answers instead of throwing. Use `LIMIT` where truncation is genuinely correct, such as showing the ten most recent items.

## Cause 2: the SOQL for loop that people think fixes this

This misconception is worth its own section because the advice is repeated so often.

```apex
// Still retrieves every matching row. Still counts every one of them.
for (Account a : [SELECT Id, Name FROM Account]) {
    // ...
}
```

The sObject-list form of a SOQL for loop processes records 200 at a time, which keeps only one chunk on the heap and is exactly the right tool for `Apex heap size too large`. It does nothing for the row limit, because every record it iterates was still retrieved by the query. If you are hitting 50,001, the for loop is not your fix.

```apex
// Correct use of the list form: bulk DML per chunk, low heap.
for (List<Contact> chunk : [
    SELECT Id, Status__c FROM Contact WHERE AccountId IN :accountIds
]) {
    for (Contact c : chunk) { c.Status__c = 'Reviewed'; }
    update chunk;
}
```

## Cause 3: parent-child subqueries, where child rows count too

A subquery looks like one query returning 200 parents. It returns 200 parents plus every child row they matched.

```apex
List<Account> accounts = [
    SELECT Id, (SELECT Id, Amount FROM Opportunities)
    FROM Account WHERE Industry = 'Technology'
];
```

With 5,000 Accounts averaging 12 Opportunities, that’s 65,000 rows from a single statement. Query the children separately with their own filter and group them into a Map, or bound the subquery with its own `WHERE` and `LIMIT` — subqueries accept both.

There’s a related failure with the same root cause and a different message. Assigning a subquery result with 200 or more child records directly to a `List` throws a `QueryException` about an aggregate query having too many rows for direct assignment. Iterating the child relationship in a for loop instead of assigning it avoids that one.

## Cause 4: many modest queries adding up

Each query returns 800 rows, which nobody would call excessive, and there are eighty of them across four handlers and a managed package.

You can find this in one pass without reading any code. Set the Apex Code log level to FINEST, reproduce the failure, and look at the `CUMULATIVE_LIMIT_USAGE` block at the end of the log. It reports the query-row total broken out by namespace in the [`LIMIT_USAGE_FOR_NS`](https://trailassembly.com/limit-usage-for-ns/) lines, and the log body lists each `SOQL_EXECUTE_END` with its row count. Sort by rows and you have your suspects.

## Cause 5: the work genuinely needs 200,000 records

Sometimes the volume is real: a nightly recalculation, a data cleanup, a migration. Then no filter will help and Batch Apex is the answer, because the `start` method’s query locator streams rather than materializing:

```apex
public class ContactCleanupBatch implements Database.Batchable<sObject> {

    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, Status__c FROM Contact WHERE Status__c = null'
        );
    }

    public void execute(Database.BatchableContext bc, List<Contact> scope) {
        for (Contact c : scope) { c.Status__c = 'Unknown'; }
        update scope;
    }

    public void finish(Database.BatchableContext bc) { }
}
```

[Governor limits](https://trailassembly.com/governor-limits-reference/) reset for every `execute` invocation, so each chunk of 200 gets a fresh 50,000 rows. The 10,000-record ceiling on `Database.getQueryLocator` applies when you call it outside a batch `start` method, that is the distinction people miss when they test the locator in Anonymous Apex and conclude it can’t work.

State the cost plainly before you choose this. The job runs when the platform schedules it, not now. It commits chunk by chunk, so a failure in the middle leaves the org half-processed. And you need somewhere to report progress, because “did it work” is no longer answerable from the caller’s return value.

## Diagnose it in code

```apex
System.debug('Query rows: ' + Limits.getQueryRows()
    + ' of ' + Limits.getLimitQueryRows()
    + ' | queries: ' + Limits.getQueries());
```

Print that between handler calls. The method where the number jumps by tens of thousands is the one to fix, and this takes less time than reading the code.

## Confirm the fix

Test data volume, not test coverage, is what proves this fixed. Insert enough records that the old code would have failed, then assert the ceiling:

```apex
@IsTest
static void reportUsesAggregateNotRowScan() {
    // Enough parents that the old subquery approach would exceed 50,000.
    TestData.accountsWithOpportunities(1000, 60);

    Test.startTest();
    Integer before = Limits.getQueryRows();
    ReportService.buildSummary();
    Integer used = Limits.getQueryRows() - before;
    Test.stopTest();

    System.assert(used < 5000, 'Query rows still scale with data volume: ' + used);
}
```

Assert a tight bound like 5,000 rather than 49,999. An [assertion](https://trailassembly.com/apex-assert-exception-message/) parked next to the limit passes until the day production data grows, which is the day you needed the test to speak up.

## Preventing the next one

- **Every query gets a filter that bounds its result set** — an Id set, a date range, an indexed status field. “All records of this type” is not a query, it’s a table scan waiting to break.

- **Reach for `COUNT()` before retrieving rows to count them.**

- **Treat subqueries as multipliers** and [estimate](https://trailassembly.com/salesforce-project-estimation/) parent count times average children before shipping.

- **Test against production-shaped volume.** A sandbox with 200 Accounts cannot tell you anything about this limit.

## Frequently asked

### Does a SOQL for loop raise the 50,000 limit?

No. It reduces heap consumption by processing 200 records at a time. Every retrieved record still counts against the row limit. Use it for heap errors, not this one.

### Does asynchronous Apex give me more rows?

No. Async doubles the query count to 200 and heap to 12 MB, but rows stay at 50,000. Batch Apex is different: it resets the limit per `execute`, which is what actually lets you process millions.

### Does `COUNT()` consume 50,000 rows if it counts 50,000 records?

No. An aggregate query returns one row, and that’s what counts. Aggregate queries are governed by their own per-transaction limit on the number of such queries.

### Do rows returned to a flow or a managed package count against my limit?

Yes. The limit is per transaction and spans everything that runs in it, which is why your code can throw the exception without being the code at fault. The per-namespace breakdown in the debug log tells you which.

### Can I raise the limit with a support case?

No. Per-transaction [governor limits](https://trailassembly.com/governor-limits-reference/) are fixed regardless of edition or licence count.

---

*Reference: [Per-Transaction Apex Limits](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm), [SOQL For Loops](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_loops_for_SOQL.htm), [Use Batch Apex](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_batch_interface.htm), and [Limits Class](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_limits.htm), Salesforce Apex Developer Guide.*

---

Source: https://trailassembly.com/query-rows-50001/
Trail Assembly — independent Salesforce write-ups.
