“This application uses 900 MB of RAM.”

In our Chromium test, that sentence can be true. It can also be deeply misleading.

We launched Chromium headless with five simple pages, waited five seconds, froze its process tree, then read each process's memory. Adding the RSS of eleven processes gives roughly 899 MiB at the median across three captures. Using the PSS of the same tree brings that down to about 278 MiB. USS is lower still, at around 194 MiB.

Same software. Same moment. Same kernel. Almost a 3.2x difference between two numbers we could both call “memory used.”

The problem is not that one counter is lying. The problem is that they do not answer the same question.12

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.

Modern software is rarely a single process

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?

RSS: the pages visible to a process

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.

Adding RSS can count the same page multiple times

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.

USS: what would disappear with this process

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.

Even fast RSS is not always a perfect 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

Taking the picture at the same 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.

What we actually measured

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: the wrapper hiding the real process

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

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: a single process, at least in this mode

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

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: when RSS becomes a spectacular addition

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

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.

A cgroup can sometimes be a better answer than a process tree

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.

The JVM adds 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.

Why application comparisons quickly turn into a contest between apples and 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.

To measure an agentic tool, you have to decide where the agent ends

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.

memftprt is interesting because it refuses 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.

The next time software “uses 1 GB,” ask 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.

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.