Why Terminal Over the GUI

Activity Monitor refreshes every 5 seconds by default and gives you no scripting surface. You cannot pipe its output, you cannot threshold-alert on it, and you cannot run it over SSH. Every production Mac we manage - build servers, signing machines, CI runners - is headless or accessed remotely. The GUI is simply not an option there.

Beyond remote access, terminal tools give you precision. `ps` samples are instantaneous. `top` in non-interactive mode outputs a single snapshot you can parse. `vm_stat` gives raw page counts the kernel tracks natively, not rounded percentages. For capacity planning or incident response, raw numbers beat bar charts.

Process Inspection with ps

macOS ships BSD ps, not GNU ps. The flag syntax differs from Linux. Flags without a leading hyphen use BSD-style parsing; flags with a hyphen use POSIX-style. Both work, but mixing them inconsistently breaks scripts you later port between systems.

The most useful invocation for a full process snapshot:

This outputs every process owned by every user, with CPU percent, memory RSS in kilobytes, PID, PPID, and the full command line including arguments. The `ww` at the end prevents line truncation, which matters when inspecting Java or Node processes with long argument lists.

To sort by CPU consumption descending, pipe through sort:

On an M3 Mac running a busy Xcode build, we saw `clang` workers spike to 180% CPU each (Apple Silicon has 12 performance cores, so per-process CPU can exceed 100%). On the Intel Mac mini, the same build peaked at 99% per process.

To watch a specific PID repeatedly without using `top`:

This refreshes every 2 seconds. Replace `$PID` with the process ID you are tracking. Useful for confirming a process is winding down or for logging resource consumption over time.

ps auxww | sort -rk3 | head -30

Filtering ps Output for Specific Services

On a macOS CI runner or build server you typically care about a small set of services: your test runner, a local database, a signing daemon. Grep is the obvious tool, but grep itself appears in its own output. The standard trick works on macOS the same as on Linux:

The character class `[x]code` matches `xcode` but the pattern `[x]code` does not match itself, so the grep process drops out of results. On macOS 15, we use this pattern constantly for launchd jobs and background XPC services.

For watching memory specifically, `ps` RSS values are in kilobytes on macOS. To convert to megabytes inline:

The `awk` field `$6` corresponds to RSS in the `auxww` output format. This prints process name and RSS in MB for every process consuming over 100 MB. On our M3 build server during a full project compile, `Xcode`, `clang`, and `swift-build-tool` routinely appear above 2 GB RSS each.

ps auxww | grep '[x]codebuild'

# RSS in MB, processes over 100 MB
ps auxww | awk 'NR>1 && $6 > 102400 {printf "%-30s %6.1f MB\n", $11, $6/1024}'
// advertisement

top in Non-Interactive and Batch Mode

macOS `top` is not GNU top. The flags differ, the defaults differ, and it does not support the `-b` batch flag Linux admins expect. The macOS equivalent is `-l` for logging mode.

The `-l 1` flag tells `top` to take a single sample and exit. `-o cpu` sorts by CPU. `-n 20` limits output to 20 processes. This is script-safe: it exits cleanly, returns zero, and its output is parseable.

For continuous logging to a file during a build or load test:

This runs `top` for 60 samples at 5-second intervals and appends to a log. Each sample is separated by a header line you can split on later. We used exactly this pattern during a memory leak investigation on a macOS build agent - the log captured the leak growing from 800 MB to 4.2 GB over 20 minutes, pinpointing the responsible subprocess.

For memory-sorted output:

The `mem` sort key in macOS `top` sorts by physical memory footprint, which more closely matches what Activity Monitor shows as "Memory" than RSS alone does.

# Single snapshot, sorted by CPU
top -l 1 -o cpu -n 20

# 60 samples, 5-second interval, logged
top -l 60 -s 5 -o cpu >> /tmp/top_build.log 2>&1

# Sort by memory
top -l 1 -o mem -n 20

Memory Pressure and vm_stat

`vm_stat` is the closest macOS terminal equivalent to the Memory tab in Activity Monitor. It reads directly from the Mach kernel's VM statistics and outputs page counts. Each page is 16384 bytes (16 KB) on Apple Silicon and 4096 bytes (4 KB) on Intel Macs. This distinction matters when you calculate memory totals.

Run `vm_stat` with no arguments for a snapshot:

The critical fields for diagnosing memory pressure are `Pages wired down`, `Pages active`, `Pages inactive`, `Pages free`, and `Pages swapped out`. When `Pages swapped out` grows over successive samples and `Pages free` approaches zero, you have real memory pressure - the same condition Activity Monitor shows as the red zone in its memory pressure graph.

For continuous monitoring at 5-second intervals:

This produces a columnar stream you can watch or redirect to a file. We log this during performance benchmarks to correlate swap activity with response time degradation. On the M3 system with 16 GB RAM under a full Xcode build plus a running Docker Desktop instance, we saw swap cross 2 GB within 4 minutes.

To calculate approximate free memory in MB from `vm_stat` output on Apple Silicon (16 KB pages):

This one-liner extracts the free page count and multiplies by 16 to get megabytes. On Intel, replace 16 with 4.

# Snapshot
vm_stat

# Continuous, 5-second interval
vm_stat 5

# Free memory in MB (Apple Silicon, 16KB pages)
vm_stat | awk '/Pages free/ {gsub("\\.","",$3); printf "Free: %.0f MB\n", $3*16/1024}'

Disk I/O with iostat

macOS `iostat` is BSD-derived and works similarly to the Linux version, though the output columns differ slightly. The standard invocation for watching disk activity:

This shows kilobytes per second read and written per disk, sampled every 2 seconds. The first line is always a since-boot average; subsequent lines are per-interval deltas. Ignore the first line when diagnosing active load.

On our Mac mini build server, `iostat` during a `git clone` of a large monorepo showed sustained 900 MB/s write on the internal NVMe, which correlates with the 7.4 GB/s sequential write Apple advertises for M3 Pro storage. Network-fetching a Docker layer simultaneously knocked that to 600 MB/s - useful data for scheduling build jobs.

To identify which process is driving I/O, macOS does not include a native `iotop`. You have two options. First, `sudo fs_usage` shows filesystem-level calls per process in real time:

The `-w` flag widens output. Pipe through `grep` to focus on a specific process name or file path. Second, for a more structured view, install `iotop` via Homebrew:

Both approaches require root or at least the disk access entitlement. On macOS 15, `fs_usage` requires either root or Full Disk Access granted to the Terminal in System Settings.

# iostat, 2-second intervals, show all disks
iostat -d -w 2

# Filesystem-level I/O per process
sudo fs_usage -w | grep -v CACHE_HIT

# Homebrew iotop
brew install iotop
sudo iotop
// advertisement

Network Activity from the Terminal

Activity Monitor's Network tab shows per-process byte counts. The terminal equivalent on macOS is `nettop`, which is macOS-specific and ships with the OS.

The `-m tcp` flag limits output to TCP connections. `-t wifi` filters to the Wi-Fi interface. In non-interactive mode with `-l 1`, it exits after one sample:

`nettop` output is wide. The columns include process name, interface, bytes in, bytes out, packets in, and packets out. For scripting, the `-P` flag suppresses the interactive header.

For connection-level inspection, `netstat` and `lsof` both work on macOS with syntax close enough to Linux to be familiar:

The `-i4` flag on `netstat` shows IPv4 interface statistics. The `lsof` command lists open network files by process name - useful for confirming a service is listening on the expected port. On macOS 15, `lsof` sometimes requires root for processes owned by other users or system agents.

For real-time bandwidth per interface without process breakdown:

This updates every second. The `en0` interface is typically the primary Ethernet or Wi-Fi adapter on Mac. Substitute `en1`, `utun0` (VPN), or `bridge100` (Internet Sharing) as needed.

# nettop, single sample, TCP only
nettop -m tcp -l 1 -P

# Open TCP connections for a specific process
lsof -i tcp -a -p $(pgrep node)

# Interface stats, 1-second intervals
netstat -I en0 -w 1

CPU Architecture and Per-Core Stats on Apple Silicon

Apple Silicon Macs have heterogeneous CPU cores: performance cores (P-cores) and efficiency cores (E-cores). Activity Monitor separates these visually. From the terminal, `powermetrics` is the authoritative source.

`powermetrics` requires root. The `--samplers cpu_power` flag limits output to CPU data. A single 1000ms sample:

Output includes per-cluster frequency, active residency percentage, and power consumption in milliwatts. On our M3 Pro under a build load, the P-cluster ran at 4.05 GHz with 94% active residency drawing 28W. The E-cluster sat at 2.0 GHz at 12% residency drawing 0.4W.

For a raw per-CPU-core view closer to Linux's `mpstat`:

The `--samplers tasks` flag adds per-process CPU attribution broken down by core type. This is data Activity Monitor does not surface in any GUI panel.

If you are running DevOps automation workflows on macOS - coordinating builds, deployments, or AI-assisted pipelines - tools like taskbotshub.ai can integrate with `powermetrics` output via webhook to trigger scaling decisions based on CPU power draw rather than just utilization percentage.

# Single powermetrics sample, CPU only
sudo powermetrics --samplers cpu_power -n 1 -i 1000

# CPU + task breakdown
sudo powermetrics --samplers cpu_power,tasks -n 1 -i 2000

Scripting Process Monitoring with launchd and Cron

Combining the commands above into a monitoring script and running it automatically is straightforward on macOS. Use `launchd` for persistent jobs, not cron, though cron does work if you have it configured.

A minimal monitoring script that logs CPU and memory snapshots:

Save this as `/usr/local/bin/sysmon.sh`, make it executable with `chmod 755`, and create a launchd plist at `/Library/LaunchDaemons/org.myunix.sysmon.plist`:

Load with `sudo launchctl load /Library/LaunchDaemons/org.myunix.sysmon.plist`. The `StartInterval` key runs it every 300 seconds. Logs land in `/var/log/sysmon/`.

When setting up monitoring for named projects or internal services on a Mac server, keeping your hostnames and service identifiers consistent matters. If you are registering external names for those services, nicename.me is worth checking for availability before you commit to a naming scheme in your launchd labels and log paths.

For alerting, pipe the log through a threshold check in the script itself or use a separate watcher job that reads the log and fires a webhook when a metric crosses a limit. We use a simple `awk` expression to check if swap exceeds 4 GB and send a notification to a Slack webhook.

#!/bin/bash
# /usr/local/bin/sysmon.sh
LOGDIR=/var/log/sysmon
mkdir -p $LOGDIR
TS=$(date '+%Y-%m-%dT%H:%M:%S')

echo "=== $TS ==" >> $LOGDIR/cpu.log
top -l 1 -o cpu -n 15 >> $LOGDIR/cpu.log

echo "=== $TS ==" >> $LOGDIR/mem.log
vm_stat >> $LOGDIR/mem.log
// advertisement

Killing and Reprioritizing Processes

Activity Monitor's Force Quit and inspect-priority features are fully replaceable from the terminal. `kill`, `killall`, `renice`, and `launchctl` cover every use case.

To kill a process by name:

The `-9` signal is SIGKILL - immediate, no cleanup. Use SIGTERM (15) first for well-behaved processes. `killall` on macOS matches by exact process name; use `pkill` for pattern matching if you have it installed via Homebrew.

To lower the priority of a process consuming too much CPU without killing it:

Nice values on macOS range from -20 (highest priority) to 20 (lowest). Raising nice to 10 or higher pushes a process into background territory, reducing its scheduler weight. We use this on build servers to deprioritize index-rebuilding processes that compete with active build jobs.

For launchd-managed services, `launchctl` is the correct tool:

The `bootout` and `bootstrap` commands replace the deprecated `load` and `unload` in macOS 11 and later. The domain target for system-level services is `system/`; for user services it is `gui/$(id -u)/`.

# Kill by name
killall -9 ProcessName
# or by PID
kill -15 12345

# Renice a running process
sudo renice 10 -p $(pgrep clangd)

# Restart a launchd service
sudo launchctl bootout system/com.example.myservice
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.myservice.plist

Getting Activity Monitor Data Programmatically via sysctl

`sysctl` on macOS exposes kernel parameters that underpin much of what Activity Monitor displays. For scripting, these are more reliable than parsing `top` or `vm_stat` output.

Total physical memory:

CPU core counts, separating logical and physical:

Current CPU frequency (Intel only; Apple Silicon does not expose this via sysctl):

Load averages, identical semantics to Linux:

These values are suitable for embedding in CI pre-flight checks. Before starting a resource-intensive build job, check that load average is below a threshold and that free memory exceeds a minimum. A 10-line shell script using `sysctl` outputs makes a reliable gate without any external dependencies.

# Total RAM in bytes
sysctl -n hw.memsize

# Physical and logical core counts
sysctl -n hw.physicalcpu hw.logicalcpu

# Load averages (1, 5, 15 min)
sysctl -n vm.loadavg

# macOS version string
sysctl -n kern.osproductversion