In Power Pages Server Logic, I allocated 32 MB of memory and the call succeeded. Three times out of three.
Then I allocated 3.5 MB and let the script run for another 300 milliseconds. It failed with this message:
The V8 runtime has exceeded its memory limit
That isn't a contradiction. It's how the limit behaves, and it explains why these failures look random: one record fails, the next one works, and one call in five breaks for no visible reason.
Here is what I measured, how you can measure it yourself, and what it means for your design.
The limit isn't documented
Microsoft Learn documents a timeout for Server Logic through ServerLogic/TimeoutInSeconds. It doesn't list a memory limit today.
It used to. An earlier version of the Server Logic overview listed ServerLogic/MaxMemoryUsageInBytes with "Maximum memory per function: 10 MB". That row was removed in November 2025 without an explanation.
So I measured it.
How to measure something you can't read
Server Logic has no heap API. You can only find the limit from the outside: allocate memory until the call fails, then narrow it down with a binary search.
The cost of any step is then a simple subtraction: how much extra memory fits without that step, minus how much fits with it.
I got three things wrong before the numbers made sense. You'd probably hit the same three:
"x".repeat(n)allocates almost nothing. The engine keeps a compact reference until you read the string. In a local test, 20 MB of text cost 0.01 MB. Read one character withcharCodeAtand it costs the full 20 MB.- Without a pause, you measure the wrong limit. More on that in the next section.
- The last log line isn't a reliable signal. Use the HTTP status and the trace of the request.
The probe and the test runner are available for download below.
The check is periodic
This is the finding that matters most. I allocated 8 MB, well above the limit, and let the script keep running for a set time. Each row is ten calls:
| Time after allocating | Passed | Failed |
|---|---|---|
| 0 ms | 10 | 0 |
| 10 ms | 10 | 0 |
| 50 ms | 10 | 0 |
| 100 ms | 3 | 7 |
| 200 ms | 0 | 10 |
| 400 ms | 0 | 10 |
The limit only applies when a check runs, and that seems to happen roughly every 100 to 200 ms. A script that goes over the limit and finishes quickly is never caught.
Without any pause, 32 MB passed three out of three times. At 64 MB, one of three passed. Everything above failed.
I don't know the implementation. The error text matches the wording in the ClearScript project, and an issue there describes exactly this: the heap size is sampled periodically, so a sudden spike between samples can go undetected. That fits what I measured, but that Power Pages uses ClearScript is my inference, not a statement from Microsoft.
What the limit is
So every measurement needs a pause. I used 300 ms. These amounts fit:
| What you keep in memory | Fits | Fails at |
|---|---|---|
| Text in blocks of 256 KB | 3,552 KB | 3,584 KB |
| Text in blocks of 64 KB | 1,664 KB | 1,696 KB |
| Text as a single block | 4,832 KB | 4,864 KB |
| Text with non-Latin-1 characters, 256 KB blocks | 1,504 KB characters | 1,536 KB characters |
| Parsed JSON objects | 832 KB of JSON | 864 KB of JSON |
There are two things to take from this:
- There's no single number. The same limit allows 1.7 MB or 4.8 MB, depending on how the memory is used.
- One special character can double a string. The engine stores a string with even one character outside Latin-1 at two bytes per character. A single bullet point in a description is enough.
Parsed objects are the most expensive. Under 1 MB of JSON fills the limit if you keep it as objects.
Where the memory goes in one call
Real code rarely holds ballast. It holds the same data several times.
A Custom API response reaches Server Logic as a string with the actual result nested inside, as strings within strings. I replayed that shape with 266 KB of content. This is the memory in use at each step:
| Step | Memory in use |
|---|---|
| Raw response received | about 1,310 KB |
| Outer wrapper parsed | about 1,500 KB |
| Body parsed | about 2,750 KB |
| Content parsed | over the limit |
| After returning to the caller | about 3,040 KB |
266 KB of JSON already reaches the limit. This doesn't mean a 266 KB call always fails. It means a check at the wrong moment would catch it.
That shows when I raise the payload size. The "with pause" column uses five calls with a 300 ms pause, the other two use ten calls each:
| Content | With pause | No pause, no log | No pause, full log |
|---|---|---|---|
| 328 KB | 5 of 5 | 10 of 10 | 10 of 10 |
| 435 KB | 5 of 5 | 10 of 10 | 9 of 10 |
| 528 KB | 0 of 5 | 10 of 10 | 7 of 10 |
| 639 KB | 0 of 5 | 10 of 10 | 4 of 10 |
| 856 KB | 0 of 5 | 10 of 10 | 0 of 10 |
The second column is the trap. 856 KB looks completely fine in a quick test, then fails as soon as anything makes the script run longer.
Logging a payload is a timing problem
The right column already shows it. Here is a direct comparison at 483 KB of content, with 80 calls each, in alternation:
| Variant | Failed | Median response |
|---|---|---|
| No logging | 0 | 546 ms |
JSON.stringify(content) only | 0 | 573 ms |
Server.Logger.Log(JSON.stringify(content)) | 22 | 926 ms |
Building the string isn't what breaks it. The log call is. It adds about 350 ms while the entire payload stays in memory, so a check is far more likely to land in that window.
There's one more catch. In these failures, the trace is often missing, probably because the log is too large for it. You lose the error message exactly when you need it.
Never log a full payload outside of a short debugging session. And if a failure has no trace, check whether your own log is the reason.
Logging isn't the only cause, though. Without any logging, the larger payloads above still fail as soon as a check lands. Removing logs buys you stability, not capacity.
MaxMemoryUsageInBytes still works
I set the undocumented setting to three values and measured each one:
| Setting | Text fits | Parsed JSON fits | Records in my test catalog |
|---|---|---|---|
| Not set | 3,520 KB | 832 KB | 30 |
| 10 MB | 3,520 KB | 832 KB | 30 |
| 20 MB | 9,152 KB | 2,048 KB | 62 |
| 40 MB | 20,928 KB | 4,608 KB | 102 |
This table tells me two things:
- The default still seems to be 10 MB. Not setting it and setting 10 MB give identical results. The old documentation was right about the number.
- 10 MB doesn't mean 10 MB is available. About a third of it can actually hold your data.
The setting works immediately. I still wouldn't build on it. It's gone from the documentation, so it may change without notice, and I haven't tested concurrent load. Use it as an emergency measure, and ask Microsoft through a support ticket before you rely on it in production.
The real fix is less data
On a real project, the results page used about 13 % of the data it loaded. Most of the payload was attributes that only one detail page displayed. Filtering the related lists in the API shrank one response from over 500 KB to about 20 KB, and the failures on the detail page were gone.
Before you tune anything, check three things:
- What does the page actually use? List the fields your front end reads, then compare that with the response.
- How many calls does one page make? That project's filtered search made a second request that loaded the complete catalog.
- Is your DEV data realistic? A failing page that works on DEV usually has less data, not better code.
The browser isn't your bottleneck. In Chrome, parsed JSON took about 1.3 times its own size in my test, and a browser tab has far more room than 3 MB. The tight limit is on the server.
A budget you can use
With default settings, on my measurements:
- Up to about 250 KB of JSON per call: You have room, even if a check lands at the worst moment. This one is my estimate, since 266 KB already reached the limit at the peak.
- Up to about 450 KB: It works, but it depends on timing, and a slow response can push it over.
- Above about 500 KB: It fails as soon as a check lands.
Measure your own response sizes, and work out how much one record costs you. That number tells you how far your data can grow. Your limits may differ from mine, so run the probe on your own environment.
Try it yourself
Download the probe and the runner:
- memory-probe.js: Server Logic with ballast, a replay path and checkpoints. Create it as
blogtest-memory-probe. - memory-probe-runner.console.js: the browser console script that runs the series. Paste it on your portal and read
window.probeResultswhen it's done.
Use a development environment only. The probe deliberately pushes the limit.
I measured this on a trial environment in the US region in November 2026. Numbers on other environments may differ: on a second tenant, the text limit was lower while the object limit was identical. If you run it, I'd like to know what you get.