SetProcessWorkingSetSizeEx with QUOTA_LIMITS_HARDWS_MAX_ENABLE on Windows Server 2022: hard-limit flag is set in _MMSUPPORT_FLAGS, but working set still exceeds the maximum

Melonia-H 0 Reputation points
2026-10-09T07:23:36.55+00:00

Environment

  • Windows Server 2022 Datacenter, OS build 20348
  • Workload: a multi-process MPI solver (10+ worker processes, several hundred MB committed each)
  • API: SetProcessWorkingSetSizeEx(hProcess, min, max, QUOTA_LIMITS_HARDWS_MAX_ENABLE)
  • The call returns TRUE for every target process (no error, no privilege failure)

The problem

The working set of the target processes still rises well above the MaximumWorkingSetSize that was just set successfully.

What I verified from the kernel side

Because the API returning TRUE doesn't prove the flag actually reached the kernel, I wrote a small driver that reads _EPROCESS / _MMSUPPORT_FULL / _MMSUPPORT_FLAGS for each target process, so the flag is observed directly rather than inferred.

Findings:

  1. MMSUPPORT_FLAGS.MaximumWorkingSetHard == 1 (i.e. Flags.u1 == 0x00C0, bit 6 + bit 7) on every worker process. → So QUOTA_LIMITS_HARDWS_MAX_ENABLE is reaching the kernel. It is not being silently dropped or downgraded to a soft limit.
  2. Even with that flag set, WorkingSetSize still exceeds MaximumWorkingSetSize, sometimes by a large margin. Example from one dump: MaximumWorkingSetSize = 15739 pages (61 MB) while WorkingSetSize = 27806 pages (108 MB).
  3. During those over-runs, the trim-related fields are all zero: TrimmedPageCount = 0, Flags.TrimmerState = 0, Flags.ForceTrim = 0, Flags.PageStealers = 0. → The balance-set manager is apparently not being invoked to pull the working set back down.

A pattern that seems important

The over-run correlates with how fast the limit is moved, not with the absolute value of the limit.

Situation Result
MaximumWorkingSetSize held steady WorkingSetSize tracks it closely, error well under 1%
MaximumWorkingSetSize lowered sharply between adjustment cycles (e.g. 380 MB → 89 MB → 61 MB) WorkingSetSize lags far behind and stays above the new limit for a long time

This makes me think the value acts as a replacement / trim target evaluated on the fault path, rather than as an allocation gate — so the working set only converges at whatever rate the process happens to touch new pages, and a large step change simply can't be honoured immediately.

Minimal repro

include <windows.h>
include <stdio.h>
int main(void)
{
SIZE_T sz = 500ULL * 1024 * 1024;
char *p = (char *)VirtualAlloc(NULL, sz, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
/* touch everything so the WS grows */
for (SIZE_T i = 0; i < sz; i += 4096) p[i] = 1;

/* now impose a hard maximum of 50 MB */
BOOL ok = SetProcessWorkingSetSizeEx(GetCurrentProcess(),
                                     1ULL * 1024 * 1024,
                                     50ULL * 1024 * 1024,
                                     QUOTA_LIMITS_HARDWS_MAX_ENABLE);
printf("SetProcessWorkingSetSizeEx returned %d\n", ok);   /* prints 1 */

/* keep touching — observe Working Set in Task Manager
   or via GetProcessMemoryInfo(...).WorkingSetSize */
for (;;) {
    for (SIZE_T i = 0; i < sz; i += 4096) p[i] = 1;
    Sleep(100);
}
}

On Server 2022 (20348) the working set stays far above the 50 MB maximum even though the call succeeded.

Questions

  1. Is QUOTA_LIMITS_HARDWS_MAX_ENABLE still meant to be a hard limit on Windows Server 2022? The documentation for SetProcessWorkingSetSizeEx says that with this flag "the limit is enforced", but the observed behaviour looks more like a trim target.
  2. Has the implementation changed relative to Windows Server 2019? A related thread (1003229) reports the same symptom and has no accepted answer.
  3. Is there any supported way to make the working set converge immediately when the limit is lowered — i.e. to force a trim synchronously — other than calling SetProcessWorkingSetSizeEx(h, (SIZE_T)-1, (SIZE_T)-1)?
  4. Is there any registry knob, policy, or cumulative update known to affect this?

What I am not asking

I'm not asking for a way to reduce committed memory (I understand that is a different mechanism — job objects / JOB_OBJECT_LIMIT_PROCESS_MEMORY). The question is specifically about the resident working set and whether these working-set limits are enforced or merely advisory on Server 2022.

Any pointers — including "this is by design" — would be much appreciated.

Windows development | Windows API - Win32
0 comments No comments

1 answer

Sort by: Oldest
  1. Taki Ly (WICLOUD CORPORATION) 5,630 Reputation points Microsoft External Staff Moderator
    2026-10-09T07:30:02.5433333+00:00

    Hello @Melonia-H ,

    Thank you for the detailed write-up. The kernel-side checks made it much easier to narrow down. I tried to reproduce the issue and went through the related documentation. Below is what I found.

    1. What I tested

    I ran your minimal repro, plus two variants closer to your setup, on:

    • Windows Server 2022 Datacenter (Azure Edition), build 20348.5622, on an Azure VM with 4 vCPU / 16 GB, memory compression off (the Server default)
    • Windows 11 Enterprise 26200, for comparison

    The scenarios:

    1. Your minimal repro: commit and touch 500 MB, call SetProcessWorkingSetSizeEx(GetCurrentProcess(), 1 MB, 50 MB, QUOTA_LIMITS_HARDWS_MAX_ENABLE), then keep touching every page in a loop.
    2. Your step-down pattern: hard max 380 MB > 89 MB > 61 MB while the process keeps touching 500 MB.
    3. A cross-process variant of your MPI layout: a controller starts 10 workers, each touching 400 MB continuously. It applies QUOTA_LIMITS_HARDWS_MAX_ENABLE to each worker's handle and steps the max down 380 > 89 > 61 MB, sampling every 500 ms.

    I read the limits and flags back with GetProcessWorkingSetSizeEx and measured the working set with GetProcessMemoryInfo.

    2. Results (identical on Server 2022 and Windows 11)

    • The trim happened synchronously inside the call. The working set went from 502 MB to 49 MB before SetProcessWorkingSetSizeEx returned.
    • It stayed at or below the maximum while the process kept touching all 500 MB.
    • On each sharp step-down it went straight to the new limit (379 > 88 > 60 MB). I did not see the lag you describe.
    • In the 10-worker cross-process test, 0 of 10 workers were over the limit at any sample point.

    So on a clean 20348.5622 system, the hard maximum behaved as a real cap for ordinary private, pageable memory. It did not behave like a trim target evaluated lazily on the fault path.

    3. What the documentation says

    • SetProcessWorkingSetSizeEx says that with QUOTA_LIMITS_HARDWS_MAX_ENABLE, "The working set will not exceed the maximum working set limit." The Remarks say the HARDWS flags "enable you to ensure that limits are enforced." Your reading of the API is correct. The documentation does not describe when a lowered maximum is applied, does not mention any registry setting or policy that affects it, and does not describe any difference between Windows versions.
    • Working Set states that the working set "contains only pageable memory allocations". AWE and large-page allocations are not included, so they cannot explain a working set above the maximum.
    • VirtualLock limits the pages a process can lock to roughly its minimum working set. With a 1 MB minimum, user-mode VirtualLock on its own is unlikely to cause an overrun of tens of MB.
    • JOBOBJECT_BASIC_LIMIT_INFORMATION notes that when JOB_OBJECT_LIMIT_WORKINGSET is in effect, "you cannot use SetProcessWorkingSetSize to change the minimum or maximum working set size of a process in a job object". For nested jobs, the smallest limit in the chain applies.
    • The thread you linked (1003229) reports the same symptom on Server 2022 only, on different hardware from the 2019 machine. It has no root cause or official answer.

    4. Answers to your questions

    1. Is it still a hard limit on Server 2022? According to the documentation, yes. In my test on a clean Server 2022 (20348.5622), it was enforced as a hard cap and applied immediately.
    2. Has it changed compared with Server 2019? I couldn't compare against 2019 in this test. What I can say is that the lazy, fault-path-only behaviour you see is not the default behaviour of the 20348 build I tested. Because this symptom has been reported on Server 2022 before (1003229), I'm not ruling out something specific to a build, hardware or workload.
    3. Is there a way to force a synchronous trim? In my tests, calling SetProcessWorkingSetSizeEx with the lower maximum already trimmed synchronously. (SIZE_T)-1, (SIZE_T)-1 or EmptyWorkingSet were not needed.
    4. Is there a registry setting, policy or update that affects this? I found no documented one.

    5. Why it may behave differently on your system

    Your minimal repro does not reproduce on a clean box, so the overrun most likely depends on what your worker processes' working set contains or how they are hosted. The candidates I would check first:

    • Pages the memory manager cannot trim: The most likely source in an MPI workload is memory registered for RDMA/NetworkDirect, which drivers lock in physical memory and often keep cached for reuse (see Caching Registered Memory). Unlike VirtualLock, driver locks are not limited by the minimum working set. If such pages are counted in the working set, that would also fit the trim counters staying at 0, since there would be nothing trimmable to remove. I haven't confirmed this; it is a hypothesis.
    • Shared pages, such as the shared-memory sections used by the MPI transport within a node.
    • Job objects: whether the MPI launcher (mpiexec/smpd, HPC Pack or a scheduler) places the workers in a job, possibly nested, with its own working-set limit.
    • Another component (the solver, scheduler or an agent) changing the working-set limits again after you set them.

    6. What would help next

    1. The exact build including UBR (winver, or UBR under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion).
    2. Whether your minimal repro alone (no MPI job running) still exceeds the limit on that server.
    3. Which MPI implementation and transport you use (MS-MPI or Intel MPI; shared memory, TCP or NetworkDirect/RDMA).
    4. Whether the workers run inside a job object, and its limits if so.
    5. A breakdown of a worker's working set while it is over the limit: This shows directly which pages are not being trimmed.

    If the standalone minimal repro does exceed the limit on your build, that points to a build- or platform-specific issue. In that case I'd recommend opening a support case with Microsoft, since a definitive "bug vs. by design" answer needs the Windows team. Please include the repro, the exact build/UBR, the hardware/hypervisor details, and a kernel dump or trace captured during the overrun.

    I hope this helps narrow it down. I'm happy to look at the VMMap or QueryWorkingSetEx output once you have it. If you found my response helpful or informative, I would greatly appreciate it if you could follow the instruction here so others experiencing similar behavior can benefit from it as well.

    Thank you.

    Was this answer helpful?


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.