Build a memory measurement you can actually compare week to week
A single Activity Monitor glance tells you nothing, because the numbers move constantly. A 0.52 second script that logs the four figures that matter turns guesswork into a trend line.
Two vm_stat samples taken minutes apart on this machine, with no change in what was running, reported free memory as 9,128 pages and then 52,869 pages. Nearly a sixfold difference from doing nothing. Any conclusion drawn from one reading of that figure is a conclusion about which second you happened to look.
The fix is to log a small set of stable figures on a schedule and read the trend. The snapshot below takes 0.52 seconds.
The four figures worth logging
Not every number in vm_stat is worth keeping. These four move slowly enough to trend and mean something when they change.
Swap used. The clearest single indicator. Memory being written to disk because it does not fit.
Compressor occupancy. How much physical RAM the compressor is consuming to hold compressed pages. Rising occupancy means growing pressure well before swap starts moving.
Compression ratio. Pages stored divided by pages occupied. Tells you how compressible your working set is, which determines how much headroom compression is buying you.
Wired memory. The only category that can never be reclaimed. Steady growth here is worth investigating because it is usually a driver or a system service.
Free memory is deliberately absent. It is the figure that drifted sixfold above, and it is the one everybody watches.
The script
#!/bin/zsh
# memsnap — one CSV line per run
# usage: memsnap >> ~/memlog.csv
PAGE=$(sysctl -n hw.pagesize) # 16384 on Apple Silicon, never assume
TOTAL=$(sysctl -n hw.memsize)
vm_stat | awk -v page="$PAGE" -v total="$TOTAL" -v ts="$(date +%FT%T)" '
/Pages wired down/ {gsub(/[^0-9]/,""); wired=$0}
/Pages stored in compressor/ {gsub(/[^0-9]/,""); stored=$0}
/Pages occupied by compressor/{gsub(/[^0-9]/,""); occ=$0}
END {
ratio = (occ > 0) ? stored/occ : 0
printf "%s,%.2f,%.2f,%.2f,%.2f",
ts,
wired*page/1073741824,
occ*page/1073741824,
ratio,
total/1073741824
}'
SWAP=$(sysctl -n vm.swapusage | sed -E 's/.*used = ([0-9.]+)M.*/\1/')
printf ",%.2f\n" "$(echo "$SWAP/1024" | bc -l)"
Columns: timestamp, wired GiB, compressor GiB, compression ratio, total GiB, swap GiB.
Reading hw.pagesize rather than hardcoding it is the line that matters most. Apple Silicon uses 16,384 byte pages against 4,096 on Intel, and every guide written before the transition assumes the smaller value. Hardcoding 4096 here would understate every figure by exactly 75 percent, and the wrong answer looks entirely plausible.
A sample of what it produces
2026-08-03T14:22:07,2.97,6.61,2.58,24.00,2.58
Wired 2.97 GiB. Compressor holding 6.61 GiB of physical RAM. Ratio 2.58, so it is storing about 17 GiB of logical data in that space. Total 24 GiB. Swap 2.58 GB in use.
Scheduling it
mkdir -p ~/bin && mv memsnap ~/bin/ && chmod +x ~/bin/memsnap
# Header, once
echo "ts,wired_gib,compressor_gib,ratio,total_gib,swap_gib" > ~/memlog.csv
# Hourly
crontab -e
# 0 * * * * /Users/YOURNAME/bin/memsnap >> /Users/YOURNAME/memlog.csv
At 0.52 seconds per run the overhead is not worth thinking about. A launchd agent is the more macOS-native approach and cron remains simpler to set up and to remove.
Reading the result
After a couple of weeks the log answers questions a single glance cannot.
Swap trending upward across weeks with the same workload means the machine is genuinely running out of headroom. Swap spiking at particular times of day points at a scheduled job rather than at your usual work. Compressor occupancy rising while swap stays flat means pressure is increasing but compression is still absorbing it, which is the early warning.
A falling compression ratio is the subtle one. It means your working set is becoming less compressible, usually because you have shifted toward media, encrypted data or already-compressed archives. Compression stops helping, and additional RAM starts helping more than it would for most people.
# Swap column over the last fortnight
tail -336 ~/memlog.csv | cut -d, -f1,6
What this cannot tell you
It reports totals, not attribution. A rising swap figure says the machine is under pressure and says nothing about which process caused it. For that you need per-process figures:
ps aux | sort -nrk 4 | head -10 # by resident memory share
The log tells you when to go looking. It does not tell you where.
It also samples at intervals, so a short spike between two samples is invisible. Something that allocates aggressively for ninety seconds and releases will not appear in an hourly log at all. For that kind of question you want continuous monitoring, which is a different tool and a different cost.
What an hourly log does well is the slow question: is this machine coping less well than it was a month ago? That question is unanswerable from Activity Monitor and straightforward from a CSV.
Turning the log into an answer
A CSV is only useful if you actually read it. Three questions cover most of what the log is for, and each is a one-liner.
Is swap growing over time?
awk -F, 'NR>1 {print $1, $6}' ~/memlog.csv | tail -168
One week of hourly samples, timestamp and swap. If the values at the end are consistently higher than at the start under comparable workloads, headroom is genuinely shrinking.
Is there a daily pattern?
awk -F, 'NR>1 {split($1,t,"T"); split(t[2],h,":"); s[h[1]]+=$6; n[h[1]]++}
END {for (k in s) printf "%s:00 %.2f GB\n", k, s[k]/n[k]}' ~/memlog.csv | sort
Mean swap by hour of day. A pronounced peak at a fixed hour usually points at a scheduled job rather than at your own work, and Time Machine, Spotlight and photo analysis are the usual candidates.
Is the compression ratio holding?
awk -F, 'NR>1 {print $1, $4}' ~/memlog.csv | tail -48
A ratio drifting downward means your working set is becoming less compressible. That is the signal that additional RAM would help you more than it would help most people, because the mechanism currently absorbing pressure is running out of room to work with.
Why launchd is the better scheduler
The cron example above is the quickest thing to set up and it is not the macOS-native answer.
launchd handles the case where the machine was asleep at the scheduled time, which cron does
not, and a laptop is asleep for a large fraction of any given week.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.techx.memsnap</string>
<key>ProgramArguments</key>
<array><string>/Users/YOURNAME/bin/memsnap</string></array>
<key>StartInterval</key><integer>3600</integer>
<key>StandardOutPath</key><string>/Users/YOURNAME/memlog.csv</string>
<key>RunAtLoad</key><true/>
</dict>
</plist>
Save as ~/Library/LaunchAgents/com.techx.memsnap.plist and load it:
launchctl load ~/Library/LaunchAgents/com.techx.memsnap.plist
launchctl list | grep memsnap
StartInterval with launchd means at least that often, and a missed interval fires once on
wake rather than being skipped. For a trend log that behaviour is what you want: a gap in the
data is less useful than a slightly irregular sample.
To stop it:
launchctl unload ~/Library/LaunchAgents/com.techx.memsnap.plist
Per-process attribution when the log flags something
The log tells you when the machine is under pressure. It says nothing about what caused it, and that is a deliberate boundary: totals are stable enough to trend, per-process figures are far too volatile to log hourly and compare across weeks.
When the log does flag a period worth investigating, these are the commands for the second half of the question.
# Ranked by resident memory share
ps aux | sort -nrk 4 | head -10
# Ranked by compressed footprint, which ps does not show
top -l 1 -o mem -n 12 -stats pid,command,mem,cmprs
The cmprs column in top is the one worth knowing about. A process with a modest resident size
and a very large compressed footprint is holding a lot of memory that is currently squeezed, and
it will become expensive the moment that memory is touched again. ps cannot show this at all,
which is why a process that looks unremarkable in ps can still be the reason the machine feels
slow.
Neither of these belongs in the hourly log. Run them when the trend line has already told you which hour to look at.
Takeaways
- The full snapshot runs in 0.52 seconds, so it is cheap enough to schedule hourly.
- Free pages moved from 9,128 to 52,869 between two samples minutes apart, so that figure cannot be trended.
- Swap used and compressor occupancy move slowly and are the two figures worth logging.
- Always read hw.pagesize rather than assuming 4096, which understates every figure by 75 percent on Apple Silicon.
Questions
- Why not just watch Activity Monitor?
- Activity Monitor shows the present moment and keeps no history you can compare. The question worth answering is whether pressure is worse than it was last month, and that needs a log.
- How often should this run?
- Hourly is plenty. At 0.52 seconds the cost is irrelevant, and hourly resolution is enough to see both daily patterns and slow trends.
- Which single number best indicates a memory problem?
- Swap used. Compression is normal and continuous, but sustained swap growth means the machine is writing memory to disk to keep up.
Sources
What we read. Distinct from what we measured, which is in the article itself.