Batchable Apex QueryLocator 50k Record Limit: Local Success vs Production Failure
0 reputation · 17 Aug 2022, 11:44 UTC
0 reputation · 17 Aug 2022, 11:44 UTC
The Database.getQueryLocator method in Batchable Apex is documented to cap each query at 50,000 records per execution. When the SOQL returns more than this threshold, the behavior differs between local scratch orgs and production environments.
In a scratch org with a modest data set, a batch that queries over 50,000 records often completes without throwing an exception, suggesting silent truncation or partial result handling. In contrast, deploying the same batch to a production org with comparable data volume typically results in a runtime exception such as “Too many query results” during the start method.
The Salesforce documentation does not clarify whether the limit enforcement is per query or per execute call, nor does it state whether a hard failure or silent truncation is expected. This ambiguity creates uncertainty for developers moving code from scratch to production.
execute call within the same batch job?33275 reputation · 17 Aug 2022, 23:00 UTC
The 50,000-record limit is not a restriction of Database.getQueryLocator itself, but a standard SOQL governor limit. When a batch job fails in production with a "Too many query rows: 50001" exception while succeeding in a scratch org, it is almost always because the start method is returning a List<SObject> instead of a Database.QueryLocator.
Confirmed Facts:
Database.getQueryLocator is specifically designed to bypass the 50,000-record limit, allowing a batch job to process up to 50 million records.List.Likely Explanation for Environment Discrepancy:
Scratch orgs often contain smaller datasets. If your scratch org has 40,000 records, a List return type will work perfectly. Once deployed to production where the dataset exceeds 50,000, the same code triggers a LimitException. This creates the illusion that the environment is enforcing the limit differently, when in reality, the data volume has simply crossed the threshold.
To resolve this, ensure your start method is implemented as follows:
public Database.QueryLocator start(Database.BatchableContext bc) {
// Correct: Returns a QueryLocator, bypassing the 50k limit
return Database.getQueryLocator('SELECT Id FROM Account');
}
Avoid this pattern:
public Iterable<SObject> start(Database.BatchableContext bc) {
// Incorrect: This executes a standard SOQL query and returns a List,
// which is capped at 50,000 records.
return [SELECT Id FROM Account];
}
To verify the failure in production, check the Apex Debug Logs. A LimitException: Too many query rows: 50001 confirms that a standard SOQL query was used instead of a QueryLocator.
One missing diagnostic detail: Are you using a custom Iterable implementation in your start method? If you are manually iterating over a collection rather than using a SOQL string, the QueryLocator bypass does not apply.
Use comments to ask for clarification. Post a solution as an answer.
4,950 reputation · 17 Aug 2022, 19:16 UTC
When Database.getQueryLocator is used, the SOQL inside does not consume the standard queryRows governor limit; instead, the platform creates a cursor that can iterate over up to 50 million rows. The 50 000‑row limit is enforced only when the start method returns a List<SObject> (or an Iterable built from a query) because that forces the SOQL to execute synchronously and count against queryRows. Therefore, a batch that works in a scratch org but fails in production is most likely returning a List in start; once the data set exceeds 50 k rows the synchronous query throws QueryException: Too many query rows. You can confirm this by checking Limits.getQueryRows() before and after calling getQueryLocator—the value should remain unchanged, whereas after a List‑returning query it will increase by the number of rows fetched.