Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Navigating Salesforce Excellence
Navigating Salesforce Excellence
One of the quieter but genuinely useful platform changes in the Winter ’27 release: Salesforce is raising the Apex heap size limits, giving developers more room before hitting System.LimitException: Apex heap size too large.
Heap size limits govern how much memory your Apex code can consume at runtime. Every variable, collection, and object you create counts against it. Batch jobs, large data transformations, and integrations that build big in-memory collections have historically been the first to hit this wall, especially in async contexts.
With asynchronous heap space more than doubling (12 MB to 25 MB), Queueable and Batch Apex jobs get significantly more headroom to process larger datasets without needing to re-architect around chunking or governor limit workarounds.
More memory means more room to scale: fewer forced refactors just to stay under the ceiling, and more breathing room for data-heavy automation.
Feature detail from Salesforce Winter ’27 release notes.
Before optimising anything, find out where you really are. Apex exposes both numbers at runtime:
System.debug('Heap used: ' + Limits.getHeapSize());
System.debug('Heap limit: ' + Limits.getLimitHeapSize());
Drop those either side of the block you suspect and the debug log tells you exactly what it costs. This is far more reliable than guessing, and it is the only way to know whether the new ceiling actually solves your problem or just moves it.
Heap is the memory holding everything alive in your transaction at once. The usual culprits, roughly in order of how often they cause trouble:
The increase gives you room, but these techniques still matter, and they are what let you process volumes no heap limit would ever cover.
Use a SOQL for loop. This is the single highest-impact change. Instead of loading every record at once:
// holds all records in memory at once
List<Account> accounts = [SELECT Id, Name FROM Account];
for (Account a : accounts) { ... }
// processes in chunks of 200, dramatically lower heap
for (Account a : [SELECT Id, Name FROM Account]) { ... }
The second form retrieves records in batches of 200 behind the scenes and lets each batch go out of scope, so heap stays roughly flat regardless of how many records you process.
Select only the fields you need. Every extra field multiplies across every row. On a large query, trimming ten unused fields can be the difference on its own.
Release references you are finished with. Setting a large collection to null makes it eligible for garbage collection:
List<Account> batch = [SELECT Id, Name FROM Account LIMIT 10000];
processAccounts(batch);
batch = null; // heap can now be reclaimed
Build strings with a list, not concatenation. Collecting parts into a List<String> and calling String.join() once allocates far less than repeated concatenation in a loop.
More headroom removes a class of irritating failures: the batch that worked on 8,000 records and died at 12,000, the integration that broke when a payload grew, the report controller that failed only for the largest account.
It does not change the underlying shape of the problem. If your code loads an unbounded result set into memory, doubling the ceiling doubles the data volume you can survive, it does not make the approach correct. The moment your org grows past the new limit you are in exactly the same position.
If you are processing genuinely large volumes, Batch Apex is still the right answer. Each execute() call gets a fresh heap allocation, which is a structurally different guarantee from a bigger single-transaction budget.
Synchronous Apex runs while a user is waiting, so Salesforce keeps its resource budget tight to protect interactive response times. Asynchronous work has no one watching a spinner, which is why it has always had roughly double the heap, and why the gap widened further here, from double to two and a half times.
The practical read: if a job is bumping against the synchronous ceiling and does not need to return a result to the user immediately, moving it to Queueable or Batch Apex gains you significantly more room than any micro-optimisation will.
There is a practical trap in the rollout that is easy to walk into. Orgs do not all move to Winter ’27 at the same moment, and sandboxes typically upgrade ahead of production.
So you can end up here: your sandbox is on Winter ’27 with the 10 MB and 25 MB limits, your production org is still on Summer ’26 with 6 MB and 12 MB. You build something memory-hungry, every test passes in the sandbox, you deploy, and it fails immediately in production against the older, lower ceiling.
Salesforce shipped a setting for exactly this: Enforce the Summer ’26 Apex heap limit. Turn it on in the nonproduction org and it behaves as though it is still under the old caps, so your testing reflects what production will actually allow.
Worth enabling in any sandbox that is ahead of production until the two are on the same release. It costs nothing and it converts a confusing production failure into an honest sandbox failure.