Windows workload memory limits and optional Process Lasso¶
Inspect the declared controls before changing the workstation:
mem
pwsh -NoProfile -File .\scripts\Set-WorkloadMemoryLimits.ps1 -Mode Test
pwsh -NoProfile -File .\scripts\Set-ProcessLassoState.ps1 -Mode Test
.\Apply-Workstation.ps1 -Mode Test -Module ProcessLasso -Plan
Use -Json on the state commands for automation. The memory service and Process Lasso are
independent, optional installations. Neither is needed to use the worker-count environment
variable. Installing Process Lasso does not install our memory service or apply our separate
responsiveness settings.
Why Windows 11 needs an explicit workload budget¶
An ordinary desktop application does not automatically receive a small, isolated memory budget that protects the rest of the workstation. User-mode programs can collectively consume the available system commit. When commit approaches its limit, allocations can fail across unrelated applications, and paging pressure can make the desktop difficult to use. A larger page file increases commit capacity; it does not contain a runaway workload. See Microsoft's explanation of system commit and page files.
Windows does provide a kernel-enforced containment mechanism: Job Objects. Our optional service assigns selected workloads to jobs with explicit commit limits. It is more accurate to say that there is no automatic per-workload OOM fence for ordinary desktop apps than that Windows has no memory-limiting mechanism. The service still leaves a detection window, does not cover every executable, and cannot reserve enough memory for all other applications.
What each control actually limits¶
| Control | Declared behavior | Boundary |
|---|---|---|
PYTEST_XDIST_AUTO_NUM_WORKERS=3 |
Three workers for xdist automatic selection | A concurrency preference, not a byte limit; explicit -n N or a project hook can override it |
| Workload memory service | 8 GiB private commit per independent Python or dotnet.exe tree |
Descendants share the root budget when assignment succeeds; multiple independent roots have separate budgets |
| Selected AI host limits | 8 GiB per matched host process | Sandbox children may break away; this is not one budget for an entire AI session |
| Process Lasso ProBalance | Temporarily adjusts CPU priorities under contention | Helps desktop responsiveness; does not enforce the 8 GiB memory budgets |
| Process Lasso Watchdog | Optional operator-defined threshold actions | Reactive rules, not an allocation-time memory fence; no watchdog rules are installed by this repo |
| Java heap options | No override declared here | Java is excluded from the memory-service target list; no managed 8 GiB Java cap exists |
The policy is in config/workload-memory-limits.psd1. Its 8 GiB means 8,589,934,592 bytes,
not 8 GB decimal and not a working-set target. The service uses Windows
process or job commit limits.
It denies allocations beyond the applicable budget rather than killing the process itself;
the application can still fail or crash when it cannot allocate memory.
Python's limit here comes from this service, not a Python memory environment variable.
PYTEST_XDIST_AUTO_NUM_WORKERS only reduces the default worker count. For Java, a project could
choose java -Xmx8g ..., but -Xmx limits the Java heap, not total JVM process memory or its
children. Native allocations, thread stacks and other JVM memory require additional headroom.
This repository does not currently set JAVA_TOOL_OPTIONS, JDK_JAVA_OPTIONS or _JAVA_OPTIONS
to impose that heap limit. See the Java launcher reference.
xdist and AI tools launching tests¶
Each xdist worker is a separate process, with its own imports, fixtures and application data. A CPU-sized worker count can multiply memory demand before the first useful test result. Several AI tasks launching separate test runs multiply it again.
For an explicit bounded run, use:
uv run pytest -n 3
# For a memory-heavy reproduction, run without xdist workers:
uv run pytest -n 0
The machine-level environment setting applies to newly started Windows processes. Restart
terminals, IDEs and AI applications after changing it so their children inherit the setting.
It affects -n auto and -n logical; an explicit -n 16 bypasses that preference. A project
pytest_xdist_auto_num_workers hook takes precedence. WSL and remote execution need their own
configuration. These rules are documented by pytest-xdist.
For an ordinary local pytest controller successfully assigned to a tree job, its workers share the same 8 GiB total, rather than each getting 8 GiB. Three independent pytest controllers can nevertheless have three budgets, totaling 24 GiB before AI hosts and other applications. AI sandboxes can introduce additional job boundaries; check assignment errors instead of assuming the complete descendant tree is covered.
AI workflows should choose an explicit worker count, avoid overlapping memory-heavy suites,
inspect commit with mem, and use the service's Test -Json to verify actual coverage. A passing
configuration check is not a guarantee against system-wide OOM. Existing service status and
assignment errors are recorded under %ProgramData%\DataWorkStationMemoryLimits.
Optional Process Lasso and its commercial license¶
For this workstation's commercial use, purchase and activate a paid Process Lasso license. Bitsum requires purchase within 30 days of commercial deployment. ProBalance is also available in the free edition, while continued use of trial-only features such as Watchdog, CPU Limiter and instance-count limits requires Pro. See Bitsum's current licensing and feature table.
ProcessLasso has Default = $false and is excluded from default and -Module All runs:
.\Apply-Workstation.ps1 -Mode Ensure -Module ProcessLasso -Plan
.\Apply-Workstation.ps1 -Mode Ensure -Module ProcessLasso
Installation is machine-wide and may require elevation. License purchase and activation are operator-managed; no key is stored in this repository. Package compliance verifies installed files and explicitly leaves license status unchecked.
Lasso adds ongoing CPU scheduling control even when a tool ignores a worker-count preference, and its governor can apply rules without leaving the GUI open. Its ProBalance and process automation complement the memory service. They do not replace it. Our separately applied responsiveness policy protects the foreground process and desktop/recovery tools, permits background child balancing, and disables SmartTrim, foreground boosting and cache/standby-list clearing. It installs no watchdog termination rule, hard CPU affinity or power-plan override.
Inspect and apply that policy only after configuring the vendor governor as a LocalSystem service:
pwsh -NoProfile -File .\scripts\Set-ProcessLassoResponsiveness.ps1 -Mode Test
sudo pwsh -NoProfile -File .\scripts\Set-ProcessLassoResponsiveness.ps1 -Mode Ensure
Ensure backs up the INI, preserves unrelated settings, and restarts the governor. For the
independent memory-service install/remove commands, precise target selection, detection gaps,
and surviving job limits, see Memory pressure.
ProBalance and Processor Group Extender¶
Keep ProBalance for temporary background priority adjustments under CPU contention. Its restraint/restoration logs help separate CPU scheduling effects from memory-pressure interventions. Foreground boosting and Efficiency Mode are not needed for our baseline.
Processor Group Extender is intended for applications confined to one processor group on large systems. Bitsum says it is unnecessary on Windows 11, which spans groups by default. A machine with 24 logical processors does not need this feature. Neither algorithm performs early-OOM recovery.
Early-OOM recovery and audit logs¶
The Windows companion to earlyoom is implemented inside our existing memory-limit service. PowerShell provides the human commands; a small compiled service performs the repeated sampling and action. It runs independently of Process Lasso. Lasso's Watchdog provides per-process usage thresholds, not the combined system-pressure decision used here.
There are two layers. Layer 1 controls selected workloads: our 8 GiB Job Object limits and xdist concurrency setting, plus optional Process Lasso rules and CPU responsiveness controls. Layer 2 watches total system pressure and considers all ordinary user applications, including Java, browsers, AI hosts and previously unknown executables. It does not depend on layer 1's executable list or Job Object membership. This catches aggregate exhaustion from parallel agents even when each individual process stays below its own limit.
Inspect current capacities and the calculated thresholds before applying the policy:
pwsh -NoProfile -File .\scripts\Get-WorkloadMemoryDiagnostics.ps1 -Action Plan
pwsh -NoProfile -File .\scripts\Get-WorkloadMemoryDiagnostics.ps1 -Action Status -Json
pwsh -NoProfile -File .\scripts\Get-WorkloadMemoryDiagnostics.ps1 -Action Candidates
pwsh -NoProfile -File .\scripts\Get-WorkloadMemoryDiagnostics.ps1 -Action Events -Last 100
pwsh -NoProfile -File .\scripts\Get-WorkloadMemoryDiagnostics.ps1 -Action Events -SinceUtc '2026-09-19T00:00:00Z' -Json
Plan, Status, Candidates and Events are read-only. Candidates uses the installed executable
and policy to list eligible processes by private bytes and count exclusion reasons. Use sudo
for elevated visibility; an ordinary invocation cannot inspect every process the service can see.
It previews eligibility even without current pressure and never requests termination access.
Edit the EarlyOom block in
config/workload-memory-limits.psd1, then explicitly apply it:
sudo pwsh -NoProfile -File .\scripts\Set-WorkloadMemoryLimits.ps1 -Mode Ensure
pwsh -NoProfile -File .\scripts\Set-WorkloadMemoryLimits.ps1 -Mode Test
Ensure rebuilds and restarts the service. The declared mode is Enforce, which can forcibly
terminate eligible applications and lose their in-progress work. Choose Observe in the declaration
to record would-terminate decisions without stopping anything, or Disabled to retain only
the allocation limits. These modes take effect after Ensure.
The default decision uses:
| Parameter | Default | Meaning |
|---|---|---|
| Available physical memory | 7% | Includes immediately reusable memory, not just empty pages |
| Commit headroom | 7% | Both this and the physical-memory threshold must be crossed |
| Emergency commit headroom | 3% | Independent trigger even if physical RAM is still available |
| Sustained pressure | 3 seconds | Pressure must persist before selecting a process |
| Cooldown | 15 seconds | At most one attempted intervention per interval |
| Candidate minimum | 256 MiB private bytes | Skip small processes unlikely to provide useful recovery |
The service samples at the configured 500 ms loop interval plus processing time. It reads physical capacity, available RAM, current commit limit/usage and page-file allocation/usage from Windows. Threshold bytes are recomputed from current capacity; RAM/page-file changes need no fixed-size rule rewrite. It does not resize the page file. Missing page-file statistics are represented as unknown; the authoritative decision uses current commit headroom. This is a Windows adaptation, not a literal comparison against Linux free swap.
Under sustained pressure it enumerates all processes and chooses the largest eligible private-byte
consumer. Mandatory exclusions protect the guard itself, system PIDs, session-zero services,
critical processes, Windows-directory images, Windows service/window accounts and processes whose
identity or memory cannot be queried. EarlyOom.ExcludedExecutables adds exact, case-insensitive
names for Royal TS, the desktop shell, Task Manager, terminals, PowerShell and Process Lasso. There is no
inclusion list. An excluded shell does not exempt its child applications. Other applications,
including foreground apps and AI hosts, may be stopped. These exclusions deliberately leave
Windows and recovery tools outside the recovery scope.
A held process handle and creation-time check prevent acting on a reused PID. The service rechecks pressure and all candidate exclusions immediately before acting and refuses an action if the pre-action audit record cannot be written.
One application process is terminated at a time, not its entire tree. The recovery action is
forced termination with exit code 0xE0000001.
Remaining workers or AI retries can allocate again. Use pytest --max-worker-restart=0 -n 3 when
automatic worker replacement would defeat recovery. There is no guaranteed OOM protection if
pressure grows faster than the polling loop or no eligible candidate exists.
The service writes earlyoom.jsonl under %ProgramData%\DataWorkStationMemoryLimits, with one
previous file retained after rotation at approximately 4 MiB. It records periodic health samples,
pressure transitions, would-terminate, no-candidate, pre-action requests, termination outcome
and recovery. Records include UTC, mode, thresholds, RAM/commit/page-file readings and candidate
PID, creation time, executable name, private bytes, session and selection scope. Older records
instead include managed-job identity from the former worker-only policy. The guard does not log
command lines, environment values or process contents.
terminated means the process exit was observed; termination-pending means the asynchronous
request has not yet been observed to finish. Match PID and creation time, not PID alone.
status.json includes the latest guard state; a fault or stale heartbeat is a coverage gap.
The event command reads a bounded tail across both log files and reports malformed/incomplete
records rather than silently treating them as no activity. Copy relevant evidence to a Tricky
case before rotation removes it. Memory/crash skills inspect these existing records before new
capture, and distinguish deliberate recovery from allocation denial or an ordinary crash.