Sample output
PID PPID RSS ELAPSED %CPU COMMAND
1847 1 812344 604812 1.4 node
2291 1847 198220 604811 0.3 node
914 1 166052 986388 0.2 postgres
3120 914 88376 84219 0.1 postgres
1102 1 61288 986402 0.0 redis-server
742 1 42116 986411 0.0 containerd
4881 4879 33940 127 12.6 rsync
688 1 18644 986414 0.0 systemd-journal
612 1 12208 986415 0.0 systemd-udevd
1 0 9884 986417 0.0 systemd
PID 1847 is holding 793 MB, and the last column of the bottom row is the context that makes it a finding. PID 1 has been up 986417 seconds, so the machine booted 11 days ago, but the node process has only been running for 604812 of them. It restarted 7 days ago and has been climbing ever since, which is the shape of a leak rather than a process that is merely large. The second row is its child by PPID, so stopping 1847 takes 2291 with it.
When you would reach for it
The box is swapping, the out-of-memory killer took something at 3am, or a container keeps getting restarted by its orchestrator and you have a few seconds to find out what was fat before it dies again. top answers the same question interactively, but it needs a terminal you can sit in front of, and it cannot be piped, logged, or dropped into a cron job. This form fits on one line of a runbook and works over ssh host '...' with no TTY at all.
Gotchas
- Add up the RSS column and you will get more memory than the machine has. RSS counts every resident page in full for every process that maps it, so a shared library loaded by forty processes is charged forty times, and copy-on-write pages a child has never written are charged to both parent and child. The honest per-process number is PSS, where shared pages are divided among their users:
awk '/^Rss:|^Pss:/{print}' /proc/1847/smaps_rollup. The gap is not subtle: check it against any idle login shell and you will see roughly 2500 kB of RSS reported against a PSS closer to 1200 kB. %CPUis an average over the process’s entire lifetime, not a current reading. In the output above,rsyncshows 12.6% after 127 seconds of life whilenodeshows 1.4% after a week, and that does not meannodeis idle. It means a week of arithmetic has flattened whatever it did this morning. For an instantaneous number, take two samples of/proc/PID/stata second apart, or usetop -b -n 2and read the second frame.- Neither
--sortnoretimesexists in the BSDpson macOS. Use-mto sort by memory andetimefor elapsed time, as in the first variant below. BSD’scommalso behaves differently: it prints the full executable path, anducommis the short accounting name. On Alpine, BusyBox provides apsthat accepts a narrow-okeyword list and has no--sortat all, soapk add procpsbefore running this. - Swap
commforargsand the output gets truncated to your terminal width, becausepsclips lines to the screen when stdout is a TTY. A pipe is not a TTY, so the same command throughlessor into a file shows the full command line and you get two different answers from one command. Add-wwto disable clipping in both cases.
Variants
$ ps -Ao pid,ppid,rss,etime,pcpu,comm -m | head -11
The macOS and BSD form. -A is the BSD spelling of -e, and -m sorts by memory usage instead of by terminal and PID
$ ps -eo rss,comm --no-headers | awk '{mem[$2]+=$1} END {for (c in mem) printf "%8d %s\n", mem[c], c}' | sort -rn | head -5
Total memory per program rather than per process. Chrome and Node fragment themselves across dozens of PIDs, so the real consumer can be invisible in a per-process ranking
$ ps -o pid,rss,etimes,pcpu,comm -p "$(pgrep -d, -f 'node server.js')"
Watch one application over time. pgrep -d, emits a comma-separated PID list, which is exactly the format -p wants, so the command substitution does the joining for you