Task Manager says Memory 62% and that number tells you almost nothing.
It hides every memory metric that actually explains why your PC stutters. ramcheck
shows the ones that matter: the hard page faults that cause the stutter, the
commit charge that causes "out of memory" errors, how much of your RAM is
reclaimable cache, and the gap between the RAM you bought and the RAM you can use.
Read-only. Zero dependencies. One PowerShell file.
This is not a hardware memory tester. It will not find a faulty DIMM — use MemTest86 for that. This tells you how Windows is using the memory you have.
powershell -NoProfile -ExecutionPolicy Bypass -File ramcheck.ps1
ramcheck - what Task Manager will not tell you about your RAM
══════════════════════════════════════════════════════════════
INSTALLED HARDWARE
32.00 GiB installed 2 modules SK Hynix 3,733 MT/s
31.60 GiB usable 413.96 MiB reserved by hardware
WHERE YOUR RAM ACTUALLY IS
in use by apps 7.27 GiB ██████░░░░░░░░░░░░░░░░░░░░
modified (dirty) 594.86 MiB ░░░░░░░░░░░░░░░░░░░░░░░░░░ queued for the page file
standby cache 3.90 GiB ███░░░░░░░░░░░░░░░░░░░░░░░ reclaimable - counted as available
free 19.84 GiB ████████████████░░░░░░░░░░
Task Manager would say 24.9% used
actually available 23.74 GiB of 31.60 GiB (75.1%)
of which reclaimable 3.90 GiB is file cache an app can take instantly
COMMIT CHARGE the number that causes "out of memory", never shown
committed 8.29 GiB
limit 36.60 GiB = 31.60 GiB RAM + 5.00 GiB page file
in use 22.6% ██████░░░░░░░░░░░░░░░░░░░░
PAGE FAULTS OVER 4.1s only hard faults touch the disk
total 4,181.2 /s
hard (from disk) 0.0 /s THIS is what causes stutter
pages pulled in 0.0 /s
soft (from RAM) 4,181.2 /s costs nanoseconds, harmless
hard share 0.000% of all faults went to disk
paged out 0.0 /s pages written to the page file
by type, as Windows counts them (these overlap and do not sum
to the total - demand zero alone can exceed it):
demand zero 5,447.1 /s
transition 1,059.8 /s
cache 11.9 /s
copy-on-write 7.8 /s
TOP PROCESSES BY PRIVATE WORKING SET
copilot 9908 342.68 MiB
svchost 14172 337.43 MiB
Discord 13188 266.43 MiB
MsMpEng 4808 254.98 MiB
msedgewebview2 1896 144.69 MiB
dwm 1796 133.97 MiB
VERDICT: ok - no memory pressure
That is real output from the machine this was built on, not a mock-up.
Because "used" counts cache that any app can take back instantly. On the machine above, 3.90 GiB of what looks occupied is standby cache — file data Windows is keeping around because the RAM was otherwise idle. It is already counted as available. A high "used" figure with a large standby cache is a machine using its RAM well, not a machine running out.
Because the number that actually fails is commit charge, and Task Manager
does not show it against its limit. When an allocation fails with "out of memory",
the limit it hit was the commit limit — physical RAM plus your page files. You
can hit it at 60% "memory used". ramcheck shows commit against that limit, and
shows the arithmetic: 36.60 GiB = 31.60 GiB RAM + 5.00 GiB page file.
Because stutter comes from hard faults, and Windows shows you neither kind. A soft fault is served from RAM and costs nanoseconds — a healthy desktop does thousands per second, forever, and it means nothing. A hard fault goes to the disk and costs milliseconds. That is the one you feel. They are completely different events and the ratio is the whole story.
| Source | Value |
|---|---|
Win32_PhysicalMemory (the SPD data on the sticks) |
34,359,738,368 = 32.00 GiB |
Win32_ComputerSystem.TotalPhysicalMemory |
33,925,664,768 = 31.60 GiB |
Win32_OperatingSystem.TotalVisibleMemorySize |
33,130,532 KB = exactly the same |
414 MiB is reserved by the hardware and is not addressable by Windows at all.
That gap appears in no Windows UI. ramcheck prints both numbers and the difference.
Demand-zero, transition, cache and copy-on-write faults overlap, and the categories can sum to more than the total. On this machine, cumulatively since boot:
Page Faults 72,152,701
Demand Zero 92,611,282 <-- larger than the total
Transition 13,208,015
Write Copies 390,168
A tool that adds those up and calls it "soft faults" is printing a number that does
not mean anything. ramcheck computes soft faults as total - hard and shows the
categories separately, labelled as overlapping.
Page Reads/sec counts read operations (one per stall). Pages Input/sec counts
the pages that arrived, which is larger because the kernel clusters read-ahead
into the same operation. Measured live on this machine during a normal desktop
session:
hard (from disk) 0.6 /s
pages pulled in 5.4 /s 8.5 pages per read
8.5 pages per read. Reporting pages as if they were faults overstates your stall
count by that factor. ramcheck reports the count and the volume as separate things.
| Verdict | Meaning |
|---|---|
ok |
no memory pressure |
warn |
something worth knowing about |
problem |
something is actively hurting you |
| Code | Fires when |
|---|---|
hard-faulting-heavy |
≥ 100 hard faults/s — this is what stutter feels like |
hard-faulting |
≥ 10 hard faults/s sustained |
commit-critical |
commit ≥ 95% of the limit; allocations are about to fail |
commit-pressure |
commit ≥ 85% of the limit |
available-critical |
< 5% of RAM available |
available-low |
< 12% of RAM available |
modified-backlog |
≥ 10% of RAM is dirty pages queued for the page file |
no-pagefile |
no page file, so the commit limit is RAM alone |
layout-inconsistent |
the counters contradict themselves — numbers withheld |
commit-inconsistent |
the commit percentage disagrees with itself |
commit-limit-unexplained |
the limit is not RAM + page files |
Exit code is 0 for ok, 1 for warn/problem, 2 for a usage error.
The last three are data-integrity checks. When two independently-derived values
of the same quantity disagree, the parse is wrong, and ramcheck says so instead of
reporting numbers it cannot stand behind.
ramcheck.ps1 # 5-second sample, human report
ramcheck.ps1 -Seconds 30 # longer window for fault rates
ramcheck.ps1 -Info # instant values only, no sampling window
ramcheck.ps1 -Procs -Top 15 # add the top memory-holding processes
ramcheck.ps1 -Json # machine-readable
ramcheck.ps1 -Json > cap.json
ramcheck.ps1 -FromJson cap.json # replay a capture, on any machine
ramcheck.ps1 -Quiet # exit code only
-FromJson reads a capture saved anywhere, so you can have someone send you their
ramcheck -Json output and analyse their machine on yours. It is also the
non-admin escape hatch and how the test suite replays fixtures.
Rate-derived fields need a time window. With -Info they come back null, never
a misleading 0 — "unknown" must not masquerade as "perfect".
Windows PowerShell 5.1 or later. No modules, no downloads, no installer. No admin rights needed for the memory data.
Execution policy blocks .ps1 on most machines. Run it with
-ExecutionPolicy Bypass as shown above — that is process-scoped and changes
nothing on your system. You do not need to run Set-ExecutionPolicy, and this
tool never asks you to.
ramcheck starts no processes, writes no files, and changes no setting. That claim
is verified two independent ways in realcheck.ps1:
- the source is scanned for all 33 mutating calls (
Set-ItemProperty,Remove-Item,SetValue(,WriteAllText,Stop-Process,Set-ExecutionPolicy,SetEnvironmentVariable, …) and must contain none — plus a negative control proving the scan can find a call that is present, so its silence means something; - the user environment block, page-file settings and execution-policy list are snapshotted before and after a run and must be byte-identical.
Because it changes nothing, there is no undo and none is needed.
Two suites, and neither one just checks that the tool printed something.
powershell -NoProfile -ExecutionPolicy Bypass -File selftest.ps1
112 passed, 0 failed
Every field in the fixtures gets a different distinctive value, so a tool that mixes two fields up cannot pass by accident — the nine fault counters are given deltas producing nine unique rates (1000, 22, 330, 4, 700, 200, 3000, 50, 10 /s), and the suite asserts those nine are distinct before relying on them.
Both sides of every threshold are tested (9.99 vs 10 hard faults/s, 84.99 vs 85% commit, 12 vs 11.9% available, 9.9 vs 10% modified). A synthetic healthy machine must produce exactly zero risks — the cry-wolf test.
Specific traps covered: a UInt64 above Int64.MaxValue (18446744073113190961,
observed in the wild — [long] throws on it, [decimal] does not); a counter that
went backwards yielding null and never 0; division-by-zero paths yielding null;
percentages clamped into [0,100].
A suite that passes proves nothing until it is shown capable of failing. mutate.ps1
makes one plausible bug at a time in ramcheck.ps1 and requires selftest to catch
every one.
powershell -NoProfile -ExecutionPolicy Bypass -File mutate.ps1
mutation score: 41 / 41 killed
Including: hard faults counted as pages, the commit percentage missing its ×100,
the availability identity hard-coded to true, the standby reserve list dropped,
in-use forgetting the modified list, each risk threshold moved so it never fires,
and each threshold moved so it always fires.
This harness earned its keep immediately. Its first run reported a survivor —
"bar clamp removed" — which turned out to be an equivalent mutant: Format-Bar
clamps both the fraction and the resulting fill count, so removing one changes
nothing observable. Rather than delete the assertion, the control was rewritten to
remove both clamps together, which is killed. It also caught one invalid
control whose anchor text did not exist in the source, and therefore had been
silently testing nothing.
powershell -NoProfile -ExecutionPolicy Bypass -File realcheck.ps1
72 / 72 passed, 0 skipped
Synthetic fixtures are clean and predictable, which is exactly why they miss things. This suite feeds the parser genuine live data and checks it against independent reference implementations using different techniques.
R1 — WMI vs PDH, field for field. Two different Windows APIs
(Win32_PerfRawData_PerfOS_Memory via CIM, and Get-Counter via PDH) reading the
same kernel data through different plumbing, across 11 fields and 8 rounds.
R2 — the tool's rate arithmetic vs PDH's own cooked values, over the identical
window: worst relative error 0.00000000% across 18 readings. PDH exposes
RawValue and SecondValue alongside CookedValue, so the tool's formula can be
run over the exact window PDH used. This is how the formula was derived rather
than guessed:
rate = Δ(RawValue) / (Δ(SecondValue) / 10,000,000)
SecondValue is the PerfTime timestamp, not a second data value. Two plausible
alternatives (Timestamp100NSec and Timestamp.Ticks) give ~0.05% error — close
enough to look right, wrong enough to be wrong. Negative control: the same
comparison with three deliberately wrong time bases is rejected in all three cases.
R3 — known ground truth planted inside a real snapshot. Distinctive values are written into a genuine 29-field live snapshot, and the tool must find exactly them while surrounded by real, irrelevant data — with an explicit assertion that no real value leaked into the result.
R4 — independent cross-checks. GlobalMemoryStatusEx via P/Invoke (a raw kernel
struct, not a counter provider) agrees exactly on total usable RAM and on the commit
limit. Win32_OperatingSystem.TotalVisibleMemorySize is in kilobytes — a classic
unit trap, so there is a negative control asserting that forgetting the ×1024 is
caught. Win32_PageFileUsage confirms the page-file total. Process names are
verified pid-by-pid against Get-Process: 199 pids cross-checked, 0 mismatches,
of which 132 raw perf names carried a #N suffix (svchost#75,
msedgewebview2#11) that had to be normalised first.
R5 — the headline feature, verified by generating the condition it detects. An API that returns a number without erroring has not been shown to measure anything; a stale value or a constant looks identical. So the suite allocates 512 MiB in a child process, touches every 4 KiB page, and requires the tool's numbers to move by the known amount:
committed rose 600.00 MiB (allocated 512 MiB)
page faults +363243, demand-zero +534250, hard reads +1 (expected ~131072 page touches)
soft:hard ratio 363,243:1
idle delta over 0.7 s: 1963 faults vs 363243 under load
The discriminating assertion is the last one: soft faults spike by a third of a
million while hard faults stay at 0 or 1. A tool that conflated the two would
show a hard-fault storm here. The allocator signals READY rather than being given a
fixed head start, because .NET has to JIT and pages are only faulted in on touch.
Use -SkipLoad to skip this test.
R6 — the tool's own -Json output fed back through a bounds check, including
the aggregate check inUse + available + modified == usable. Every individual
reading can be clamped and an aggregate still be wrong. Also verifies
free + standby == available, the standby sub-lists summing to the standby total,
-FromJson replay preserving every value, a UTF-8 BOM not breaking the replay,
and the error paths emitting valid JSON with exit code 2 rather than a stack trace.
R7 — the read-only proof described above.
Machine-state tests that pass on a cool, quiet machine can fail on the same machine
60 seconds later. Both suites were run repeatedly back to back: 4 consecutive
runs at selftest 112/112 and realcheck 72/72, then 3 further realcheck runs
after the R1 rework — 7 green runs of the real-data suite in total.
The first version of R1 compared WMI and PDH and demanded exact agreement. It failed, and why it failed is the most interesting thing in this repository.
These counters are not monotonic counters. They are level gauges that oscillate in quantised 4 KiB page steps around an equilibrium. Two APIs physically cannot be sampled at the same instant, so exact comparison is measuring the machine's stillness, not the tool's correctness.
The obvious fix — bracket the PDH read between two WMI reads and treat "both WMI reads identical" as proof the field held still — is also wrong, and the machine disproved it inside one run:
[MISMATCH] AvailableBytes wmi=25479143424 pdh=25479208960 (65,536 = 16 pages)
[MISMATCH] FreeAndZeroPageListBytes wmi=21442437120 pdh=21442502656 (65,536 = 16 pages)
[MISMATCH] PoolPagedBytes wmi= 306765824 pdh= 306749440 (16,384 = 4 pages)
Every divergence is a whole number of pages. A process that allocates 16 pages and frees them again inside the bracket returns the gauge to a byte-identical value while genuinely having moved. Bit width is irrelevant.
So fields are classified empirically by what they actually did during the run:
- static — never varied in either API. Compared exactly. This is the decisive claim, and it carries the full negative control including single-byte corruption.
- oscillating — varied, and therefore cannot be compared exactly by anyone. Checked against a self-calibrating tolerance: the observed width of the WMI bracket is how much the machine churned between reads, so the tolerance is derived from it (floor 4 MiB, else 4× the observed churn). A quiet machine gets a strict test; a machine mid-game gets a fair one.
The two claims are counted and reported separately, because claiming "0 exact mismatches" about a gauge that cannot be exactly compared would be a lie:
EXACT: 2 static field(s) agree byte-for-byte between WMI and PDH
BOUNDED: 72 oscillating observations within their self-calibrated tolerance
mutation score 64/64; 6 equivalent mutants excluded
worst divergence outside the bracket: 6127616 bytes (1496 pages) on CommittedBytes
The tolerance-based claim gets a restricted mutator set — only corruptions
larger than the tolerance (wrong scale, wrong field, doubled, halved, zeroed, sign
flipped), all of which are orders of magnitude above it. A sub-tolerance corruption
is not detectable on an oscillating gauge, so the suite says so explicitly and
prints any such mutant as [UNDETECTABLE] rather than quietly counting it as a pass.
Equivalent mutants (zeroing a field that was already zero) are excluded from the
score rather than counted, because counting them would inflate it.
Win32_PerfRawData_PerfOS_Memory's*Persecfields are cumulative counters, not rates, despite the name.PageFaultsPersecreads72152701— that is the total since boot. You must difference two samples yourself.Memory Compressionis invisible toGet-Processand toWin32_PerfRawData_PerfProc_Process(which returns zero rows for it). Task Manager uses undocumented APIs.ramcheckreports it asnulland says Windows does not expose it — a confidently wrong diagnosis is worse than none.- PDH counter paths come back lowercased (
\\machine\memory\available bytes) while WMI uses PascalCase field names. Route every lookup through one canonical form or half your data attaches to nothing. [int][math]::Pow(2, 31)throws. Any 32-bit flags field needs[long].- A raw perf-counter field can be a UInt64 above
Int64.MaxValue.[long]throws;[decimal]represents every UInt64 exactly and still allows signed subtraction. - Under
Set-StrictMode,(<pipeline>).Countthrows when the pipeline yields a single object. Always@(<pipeline>).Count. '{0:N1}' -f 2650yields"2,650.0"—Ninserts a thousands separator. UseFfor any exact string comparison in a test.$anInt + ' some text'throws in PowerShell, because the left operand picks the operation.[string]$anInt + ' some text'is what you meant.- Dot-sourcing a script imports its
param()type constraints into your scope. After. .\ramcheck.ps1, a later$Json = @(1,2)in your script throws "Cannot convert System.Object[] to SwitchParameter" and blames your file. Every local in the test suites here is prefixedt_/r_for exactly this reason, andramcheck.ps1takes a-NoRunswitch so it can be dot-sourced safely.
ramcheck.ps1 is pure ASCII on disk — 0 bytes above 127, asserted by the test
suite. The box-drawing characters in the output are built from code points at
runtime, so the file needs no BOM and cannot be corrupted in transit or misread as
ANSI by PowerShell 5.1.
If your console cannot render them, it falls back to ASCII automatically: each glyph
is round-tripped through [Console]::OutputEncoding and replaced with #/./= if
it does not survive. Both branches are tested — verified rendering Unicode on UTF-8
and code page 437, and falling back cleanly on US-ASCII (20127) and Latin-1 (28591).
- obs-4k60-recorder — OBS settings for real 4K60 capture
- gpucheck — whether the GPU was the bottleneck
- cpuclock — what your CPU is actually clocked at
- diskrate — whether the disk was the bottleneck
- framecheck — find dropped frames in a recording
MIT — see LICENSE.