“This application uses 900 MB of RAM.”
Before checking that number, we need to know what it counts. RSS, PSS and USS are not three chunks of memory to add together. They are three different accounting methods applied to the same resident pages.
Imagine a process with 100 MiB of private pages and 60 MiB of pages shared by three processes in total. RSS may count all 160 MiB visible to the process. PSS counts the 100 MiB private plus only its share of the shared pages, about 20 MiB: 120 MiB. USS keeps only the 100 MiB of private pages.
| Metric | What it counts | Useful question |
|---|---|---|
| RSS | All resident pages visible to the process, including shared pages | How much resident RAM is mapped into this process? |
| PSS | Private pages + a proportional share of shared pages | How much physical RAM can reasonably be attributed to this process or workload? |
| USS | Private resident pages only | How much private RAM would disappear with this process? |
So RSS + PSS + USS is meaningless. It counts the same memory several times using three different accounting rules.
For the rest of this article, when we write “RAM used by the application,” our default will be the PSS of its complete process tree. This is not a universal truth hidden inside Linux. It is our operational definition: it includes descendants and assigns the application a proportional share of physical pages it shares. For a properly isolated service or container, cgroup memory.current can provide an even better boundary.23
That is the final number the first version of this investigation was missing.
For our headless Chromium with five pages, the answer therefore becomes: about 278 MiB of RAM used, under our definition and at the median of three captures. The ~899 MiB RSS and ~194 MiB USS remain useful for understanding that footprint, but they are not added to those 278 MiB.
Same software. Same moment. Same kernel. Three different accounting methods.
A small Linux project called memftprt offers a good excuse to revisit these questions. It does not only measure the memory of a PID. It follows an application's complete process tree, waits for a comparable state, freezes the processes, then reports RSS, PSS, USS, anonymous memory, mapped files, shared memory and swap.1
We compiled it and used it on three local cases: IRZ's Astro development server, OpenCode in server mode, and Chromium with five pages. Not to establish a ranking. To see how much the choice of scope changes the story.
Behind the first PID
When you run npm run dev, the PID shown by the shell is not necessarily “the application.”
In our Astro capture, the root process was npm. It used about 68 MiB of RSS. If we stopped there, we could announce that an Astro development server consumes around 70 MB.
The captured tree contained four processes. Most of the work lived in a child Node process, accompanied notably by esbuild. Once the complete tree was included, the total rose to around 473 MiB of RSS and 397 MiB of PSS.
The root PID therefore represented only a small part of the service we had actually started.
This is common in development tools. An npm wrapper launches a shell. The shell launches Node. Node may launch a compiler, a language server, a browser worker or other processes. IDEs and browsers go even further: one renderer per tab or site, GPU processes, network utilities, extensions, crash handlers and supporting services.
Chromium explicitly relies on a multi-process architecture to isolate and parallelize different tasks.4
Asking “how much memory does Chrome use?” while looking at an arbitrary PID is therefore a bit like estimating an entire workshop's electricity use by measuring only the power strip near the door.
The first question is not RSS or PSS.
It is: what do we call the application?
The visible pages
RSS stands for Resident Set Size.
Broadly speaking, it is the amount of memory currently resident in RAM and mapped into a process's address space. It is the figure commonly found in ps, top, /proc/PID/status and many monitoring tools.2
RSS is useful. If a single process mainly owns private pages, it can provide a very good intuition for its resident footprint.
OpenCode gave us an almost ideal case.
We launched:
opencode serve --hostname 127.0.0.1 --port ... --pure
Across three captures, memftprt saw only one process. Median RSS was around 292 MiB, PSS around 255 MiB and USS around 221 MiB. Since there were no descendants, “root process” and “complete tree” were exactly the same scope.
The difference between RSS and PSS still existed, but it was much less dramatic than in Chromium.
The RSS problem mainly begins when you add up processes that share pages.
One page, three counts
Take a 30 MiB shared library used by three processes.
Each process may have those pages in its RSS. If you naively add the three RSS values, the library may contribute close to 90 MiB to the total even though some of its physical pages are shared.
That is not an RSS bug. From each process's point of view, the pages are indeed mapped and resident.
It is the addition that answers a different question from the one we thought we were asking.
Linux exposes PSS, or Proportional Set Size, through smaps and smaps_rollup. For a page shared between several processes, the cost is divided among the processes mapping it.2
If three processes genuinely share a page under the same conditions, each carries only a fraction of it in its PSS.
This mechanism explains much of our Chromium difference.
The tree's RSS sum was around 898 to 900 MiB across the three captures. The tree's PSS varied more, from 260 to 333 MiB, with a median of about 278 MiB. USS ranged from 193 to 211 MiB.
Saying “Chromium used 900 MB” without specifying that we are adding the RSS of eleven processes therefore creates a very different picture from “the Chromium tree had about 278 MiB of PSS.”
Both sentences use real numbers. Only one really says what it measures.
What would disappear
USS, or Unique Set Size, removes shared memory again.
It keeps only the process's private resident pages. memftprt describes it as the memory that would be freed if that process alone disappeared, while other users of the shared pages continued running.1
In our median Chromium capture, the tree's total PSS was about 278 MiB and USS around 194 MiB.
The gap, about 83 MiB, represents the approximate scale of the proportional share of shared mappings attributed to the tree in that capture. This is not “fake RAM.” These are pages that exist, but whose ownership is shared with other mappings.
PSS and USS therefore answer two slightly different questions:
- how much physical memory can reasonably be attributed to this application by sharing the common cost?
- how many resident pages are genuinely private to it?
Both are useful when diagnosing a large process.
For writing a tweet that says “my tool uses X GB,” neither is magical without the method behind it.
The imperfect snapshot
The Linux kernel adds another small administrative delight.
Its documentation warns that some fast counters such as VmRSS in /proc/PID/status may be imprecise because accounting is updated asynchronously. For more detailed measurement, smaps walks the memory mappings; smaps_rollup provides an aggregation of the same type of statistics at process level.2
This difference explains why two system tools can sometimes show similar but non-identical numbers at the same moment.
Time matters too.
A program allocates, frees, fills caches, compiles code, opens a tab and terminates a worker. If we read the parent and then its children one after another, the state may change during collection.
memftprt tries to treat this as a measurement problem, not merely a /proc parsing problem.1
Freezing the moment
By default, memftprt samples the tree every 500 ms and waits for it to remain stable for three seconds. Its criteria include no process actively using the CPU or blocked in uninterruptible sleep, very little CPU change, near-stable RSS and virtual size, and the same set of PID/start-time identities.1
Once this window is reached, it freezes the tree.
When a clean delegated cgroup v2 is available, the tool can use its freeze mechanism. Otherwise it falls back to a ptrace/SIGSTOP-based mechanism, taking care to resume only the identities it had stopped.1
The kernel does expose cgroup.freeze to stop a cgroup and its descendants.3
In our tests on MJ/WSL, memftprt used the signals backend. We chose a fixed five-second delay instead of quiescence detection, so that the three application families would be photographed at the same relative moment after launch.
This is not the only valid method. It is simply the method used in our experiment.
And that sentence should accompany far more memory benchmarks.
Three snapshots
Our tests on August 12, 2026 do not attempt to compare Astro, OpenCode and Chromium with one another. Their workloads have nothing in common.
They are meant to show three forms of application.
Astro behind npm
Command: IRZ development server on a dedicated port.
Usable capture: 4 processes.
npm root process:
- RSS: ~68.0 MiB
- PSS: ~31.8 MiB
- USS: ~18.4 MiB
Complete tree:
- RSS: ~472.8 MiB
- PSS: ~396.9 MiB
- USS: ~368.8 MiB
8% of the RAM attributable to the tree is visible in the root process. Memory outside root process: 92%.
Under our definition, Astro therefore uses about 397 MiB of RAM in this capture. The npm root process represents only 8% of it. Looking only at the root PID leaves about 92% of the tree's PSS footprint outside the frame.
An earlier capture from the same run showed about 472 MiB RSS and 396 MiB PSS for the tree, which confirms this order of magnitude. However, we did not obtain three clean repeated captures for Astro, so it would be dishonest to call these numbers a three-trial median.
The only lesson is scope: looking at npm alone misses most of the application that was launched.
OpenCode at rest
Command: opencode serve --pure, with no active agent task.
Three captures, one process each time.
Medians:
- RSS: ~292.0 MiB
- PSS: ~254.7 MiB
- USS: ~220.5 MiB
100% of the RAM attributable to the tree is visible in the root process. Memory outside root process: 0%.
Under our definition, OpenCode uses about 255 MiB of RAM while idle in this test. The root process accounts for 100% of the observed tree.
Here, following the whole tree adds nothing because there was no child process at capture time.
This is a good counterexample to our own article. “You must always add up the tree” would be another silly rule. First look at how the software is organized.
And above all, this figure does not describe “how much OpenCode uses while coding.” The server was idle. A session with local models, tools, browsers or subprocesses may produce a different structure.
Chromium multiplied
We launched Chromium headless through Playwright, created five pages each containing a simple synthetic document, then kept the browser open.
Three captures, eleven processes each time.
Median of the Node/root launcher process:
- RSS: ~130.4 MiB
- PSS: ~92.2 MiB
- USS: ~84.5 MiB
Tree median:
- RSS: ~898.6 MiB
- PSS: ~277.6 MiB
- USS: ~194.4 MiB
33% of the RAM attributable to the tree is visible in the root process. Memory outside root process: 67%.
Under our definition, Chromium therefore uses about 278 MiB of RAM. The root launcher accounts for only 33%: roughly 67% of the attributable footprint lives in descendants.
The RSS sum therefore tells the story of a browser approaching a gigabyte. PSS tells the story of about 278 MiB of physical memory proportionally attributable to the tree in this snapshot. USS says that around 194 MiB was genuinely private.
This is not a Chrome benchmark. The pages were artificial, the browser was headless, and the environment was WSL. It is a controlled demonstration of the accounting problem.
And it works almost too well.
The cgroup boundary
Following parents and children is not the only way to define scope.
Linux cgroups offer a different boundary: put the workload in a group, then measure that group. In cgroup v2, memory.current exposes the total amount of memory currently used by the cgroup and its descendants.3
For a container, a systemd service or a workload that can be isolated cleanly, this unit may be more natural than reconstructing PPID relationships.
It also solves certain cases where a process daemonizes, reparents itself or shares resources in a way that makes the logical tree less obvious.
memftprt itself can rely on a delegated cgroup to freeze the workload when one is available.1
This highlights an important idea: there is no hidden “application RAM” counter somewhere in Linux. You first choose a boundary, then a metric suited to the question.
PID, process tree, cgroup, container, entire machine: each is a different scope.
Another language
memftprt also has an adapter for HotSpot Native Memory Tracking.1
The JVM can talk about heap, classes, threads, code cache, garbage collector, reserved memory and committed memory. These categories are extremely useful for understanding what the runtime is doing.
But they should not be added to PSS or RSS.
Some of the same pages are simply being described from two different viewpoints. The operating system talks about page residency and ownership. The JVM talks about the runtime's internal organization.1
PSS + NMT committed is therefore not “the true total memory.” It is often double counting with the addition neatly printed.
The same conceptual trap appears again: two numbers can each be correct and become wrong once combined without understanding what they represent.
Apples against radiators
Comparisons such as “editor A uses 300 MB, editor B 1.2 GB” circulate regularly.
They can be useful when the protocol is sound. They become theatre when nobody specifies:
- software version;
- system and kernel;
- workload;
- delay after launch;
- number of documents, tabs or projects;
- extensions and plugins;
- cold or warm caches;
- included processes;
- metric used;
- swap;
- number of repetitions.
A simple change of scope can move the result by several hundred megabytes.
Our Astro test is a caricature of this. Measuring only the npm process gives 68 MiB RSS. Measuring everything it launched gives nearly 473 MiB RSS. No optimization took place between the two measurements. We simply looked at more processes.
The most rigorous comparison is therefore not the one displaying the most decimal places. It is the one that makes the protocol clear enough for someone else to reproduce the same question.
Where does the agent end?
This question becomes even more entertaining with coding agents.
A CLI agent can be a single idle process, like OpenCode in our test. During a real task, it may launch git, npm, a type checker, a browser, tests, a development server or even a local model server.
Should all of that count as “the agent's memory”?
If the goal is to measure the agent program itself, probably not.
If the goal is to know how much RAM is required to run the workflow the agent triggers, probably yes.
And if the model runs in LM Studio on another machine, should it be included? Again, the answer depends on the question.
The term “agent” describes a logical responsibility. The kernel sees processes, pages, sockets and cgroups. It does not know that Chromium was opened because a model decided to check a page.
Measuring an agentic system therefore requires documenting not only the metric, but also the chosen functional boundary.
This is a subject we want to explore further with real workloads. The small OpenCode test here mainly serves as a control: an agent can be very simple while idle and become an orchestrator of dozens of processes as soon as it starts working.
Refusing false simplicity
The memftprt repository is young. It was published in early August 2026 and does not yet have the track record of a system tool installed everywhere. We should not turn three commits and a test suite into a universal measurement standard.1
Its idea is nevertheless sound.
It produces several figures instead of mysteriously choosing one. It preserves per-process detail. It records how the tree was frozen. It distinguishes an absent field from zero in the JSON. It preserves the statuses of its runtime probes. It explicitly explains that RSS, PSS, USS and NMT should not be mixed like compatible Lego bricks.1
The software does not solve the question “how much RAM does my application use?”
It forces us to rephrase it correctly.
Which application? At what moment? Under what load? Which pages should be attributed to whom? Are we looking for private memory, physical pressure attributable to the workload, the sum of resident mappings, a container limit or the internal structure of a runtime?
Once these questions have been asked, the number becomes much less magical.
Which gigabyte?
Our test Chromium had almost 900 MiB of summed RSS. It had about 278 MiB of PSS across the tree. Its USS was around 194 MiB.
Our answer to “how much RAM does Chromium use in this test?” is therefore 278 MiB. That is the PSS of the complete tree. The 900 and 194 MiB figures are two other views of the same pages, not quantities to add.
These numbers do not cancel one another out. They describe three views of the same moment.
The right reflex is therefore not to replace RSS with PSS everywhere and declare the problem solved. RSS remains useful. USS remains useful. memory.current from a cgroup may be even more relevant in other situations.
The useful reflex is smaller: never accept a memory figure without asking about its scope.
That is less satisfying than ranking applications from lightest to most bloated. It sometimes requires reading /proc. And there is always one more Chromium process somewhere to complicate the picture.
But at least, when we say that software uses 900 MB, we will finally know which 900 MB we mean.