SSD health

Which App Is Writing to My Mac’s SSD?

Activity Monitor gives you a suspect, not a conviction. To actually name what’s burning your SSD writes you join four layers — the process, the file paths, the device counter, and a baseline. I’ll show the whole evidence chain: Activity Monitor and fs_usage ship with macOS, and the device layer adds the free, open-source smartmontools.

Which app is writing to my Mac’s SSD?

Activity Monitor names running processes and their accumulated write totals — comparing two readings a few minutes apart shows whose counter is climbing now, since the largest lifetime total may be idle; fs_usage shows the live filesystem events and paths; NVMe SMART shows the device’s lifetime host writes but cannot name the app; and a time-series baseline catches an anomaly after the moment has passed. You join those four views by time and workload, not by pretending their byte counts describe the same layer.

“Writes” in Activity Monitor, a write line in fs_usage, and Data Units Written in an NVMe log are related, but not interchangeable. One belongs to a running process, one is a live filesystem event, and one is a controller counter. Physical NAND traffic sits below all three and includes controller behavior the host normally cannot see.

The short version

  • Apple: Activity Monitor names the process and shows accumulated Bytes Written — not NAND wear.
  • Apple: fs_usage needs sudo and shows live filesystem calls and the actual paths.
  • NVM Express: Data Units Written is lifetime host-to-controller traffic; it cannot name an app.
  • A repeated baseline catches a transient anomaly that a single snapshot can miss.
  • Join all four layers by time and workload; do not force their byte counts to match.

Most advice stops at “sort Activity Monitor by Bytes Written.” That is a decent first move and a bad last move. A large total may belong to a process that is idle now; a helper may write on behalf of a parent app; buffered data may reach the device after the process goes quiet. If the event happened overnight, the useful process row may already be gone. There is no perfect attribution tool on macOS — there is a defensible evidence chain.

FOUR LAYERS · FOUR DIFFERENT QUESTIONS 1 · PROCESS Activity Monitor Which process hasaccumulated bytes? can’t say: the path,one device, or NAND 2 · PATH fs_usage Which file is ittouching right now? can’t say: lifetimetotals or NAND 3 · DEVICE NVMe SMART How many host writesreached the controller? can’t say: which app,or write amplification 4 · TIME Baseline Is this unusualfor this Mac? can’t say: root causewithout 1–3 Not four ways to measure one number — four joins in one investigation: process, path, device, time.
None of the four names the writer on its own. You join process → path → device → time; you never assume one layer’s bytes equal another’s.

Four attribution layers, four different questions

LayerQuestion it answersTime scopeWhat it cannot prove
Activity Monitor → DiskWhich running process has accumulated OS-accounted bytes?Current process lifetime; compare two readings for a rateThe file path, one physical device, or NAND writes
fs_usage -f filesysWhich process is touching which filesystem objects right now?Live trace while the event occursLifetime totals or physical flash traffic
NVMe SMARTHow many host writes reached this controller?Lifetime counter; two readings give a window deltaWhich process caused them, or hidden write amplification
Time-series baselineIs this behavior unusual for this Mac doing this work?Repeated, labelled windowsRoot cause without process and path evidence

These are not four ways to measure one number. They are four joins in a small investigation: process, path, device, and time.

Layer 1: Activity Monitor gives you a process counter, not a rate

Open Applications → Utilities → Activity Monitor → Disk, choose View → All Processes, and sort by Bytes Written. Apple documents the Disk view as showing how much data processes read and write; All Processes includes apps, background processes, and macOS processes — not just visible windows. (Apple Activity Monitor User Guide)

The important word is accumulated. Bytes Written is attached to the running process; it is not “bytes per second right now.” If an app has been open for days, its row has had days to grow. If it exits and relaunches, that old process lifetime is over. Activity Monitor is a live process view, not durable history.

Take two readings. Record the name and Bytes Written value, wait through a known observation window, then record them again:

process write delta = later Bytes Written - earlier Bytes Written

A process sitting at 80 GB but adding nothing is historical noise for this incident. A lower row climbing steadily during an otherwise idle window is current evidence.

Do not promote it beyond that. The column is OS process accounting, not a ledger for disk0. It has no file path and does not say whether writes were buffered, sent to another volume, or expanded below the filesystem. It is not physical NAND wear. Activity Monitor gives you a suspect and a delta, not a conviction.

Layer 2: fs_usage shows the live calls and the paths

When the process counter is moving, fs_usage is the sharpest built-in way to ask what that process is touching now:

sudo fs_usage -w -f filesys

Apple says fs_usage streams filesystem-related system-call activity and requires root privileges because it uses kernel tracing. -w forces the wide layout; -f filesys filters to filesystem activity. (Apple, “Examining File-System Usage”)

But filesys does not mean writes only. It includes reads, opens, metadata calls, page activity, errors, and writes. The first trace is loud by design. Capture broadly before narrowing to the name you expect; filtering too early is how a helper disappears from the evidence. (One caveat: by default fs_usage excludes Terminal and the shells — sh, zsh, tcsh — so if a shell script is the suspected writer, name it or its PID explicitly.)

Read each line as fields:

  • A timestamp says when the event occurred.
  • CALL names the operation, such as open, read, write, writev, rename, unlink, or fsync.
  • F= is a file descriptor.
  • B= is bytes requested by that call, shown in hexadecimal — not physical flash bytes.
  • A bracketed value is an error number; a pathname appears when the event exposes one.
  • The elapsed-time field may end in W when the process was scheduled out during the activity.
  • The final field is the process; wide mode appends a thread identifier.

Apple’s examples expose an important wrinkle: a write row can show only a descriptor and byte request, while the nearby open row carries the path. Join them by the same process and file descriptor within the live trace. Keep surrounding open, rename, unlink, truncate, and fsync events; a save is often a sequence, not one neat write /path/file line.

Search the saved trace for CALL values write and writev, then inspect neighboring path-bearing events. Do not treat open alone as proof that bytes were written, or fsync as a byte count. Memory-mapped activity and later page writeback may not present as the simple call sequence you wanted.

Once the broad trace points to a process, fs_usage accepts a name or PID. Apple’s man-page example uses a process name — fs_usage -w -f filesys Mail — and here I add sudo and a ten-second bound with -t:

sudo fs_usage -w -f filesys -t 10 Mail

That captures ten seconds for processes named Mail. Replace Mail with the exact suspect; keep the broad capture because parent and writer may not share a name.

For a transient event, preserve the stream. Apple recommends redirecting it for later analysis. This example saves a bounded 30-second trace to your home folder (not ~/Desktop or ~/Documents — with iCloud Desktop & Documents syncing on, files there upload automatically, and this trace can contain usernames, document names, and app-container paths):

sudo fs_usage -w -f filesys -t 30 > ~/fs-usage.txt

Thirty seconds is an observation window, not a threshold. Keep the raw copy in a non-synced location; redact a duplicate before sharing it.

One caveat if you are also bracketing the device counter in Layer 3: writing the trace to the internal SSD advances the same Data Units Written you are trying to measure. For a clean device correlation, redirect the trace to an external volume, or treat an internally saved capture’s device delta as a contaminated upper bound rather than a measurement.

What fs_usage gives you is process + operation + time + often a path. It does not give you NAND traffic. Apple’s Instruments documentation separates logical filesystem calls from physical storage activity and warns that their sizes do not necessarily correlate. (Apple, “Reducing disk writes”)

Layer 3: NVMe SMART gives you the device total, not the writer

The controller view is the opposite of fs_usage: strong device scope, no process identity. With current smartmontools (smartctl is its command, installed via brew install smartmontools — it is not built into macOS), confirm the internal physical disk, then read it:

brew install smartmontools
diskutil list internal physical
smartctl -a disk0

On the Apple-Silicon Mac measured for this article, smartmontools 7.5 read the internal NVMe health log natively:

Data Units Written: 66,712,577 [34.1 TB]
Percentage Used:    2%
Power On Hours:     1,421

Those are facts from that Mac, not a universal baseline. disk0 was its internal device; use the identifier diskutil reports on yours. The smartmontools Darwin implementation has native NVMe log-page access and documents smartctl -a disk0 as a macOS example, but that does not promise identical access through every external enclosure or system version. (smartmontools Darwin source)

NVM Express defines Data Units Written as host data written to the controller, reported in thousands of 512-byte units and rounded up. One raw unit is 512,000 bytes. For two readings around an event:

host-byte delta ≈ (DUW after - DUW before) × 512,000

This is a lifetime device counter until you subtract two snapshots. It cannot name a process. It also cannot reveal physical NAND writes: SNIA defines write amplification as NAND writes divided by host writes, while controller garbage collection and wear levelling sit on the hidden side of that ratio. (NVM Express Base Specification); (SNIA endurance paper)

Use SMART here to confirm that the internal device counter changed during the wider event window. Our companion guide on what SMART can and can’t tell you covers health warnings, Percentage Used, and the limits of failure inference. A low wear reading does not prove an app is behaving well; a large host-write delta does not prove hardware danger.

Layer 4: a baseline catches what the snapshot missed

A snapshot asks “what can I see now?” A baseline asks “what changed?”

Record comparable windows with a timestamp, workload, process deltas, relevant paths, and device delta. Keep idle windows separate from builds, imports, exports, restores, updates, virtual machines, and sync jobs. The aim is not one normal number; it is the shape of this Mac doing this class of work.

The interesting event is often over: a helper wrote aggressively, exited, and vanished from Activity Monitor. NVMe retained the bytes but lost the name. A saved fs_usage trace retained the paths only because it was running. A time series preserves the bridge between a process leaving its usual lane and the device slope changing.

If the question becomes “is this write rate normal?”, use the sibling method for reading a Mac SSD write rate against its own baseline. An anomaly means behavior differed from baseline. It does not, by itself, mean danger.

How to find what’s writing to your Mac’s SSD

Here is the complete manual workflow. Nothing in it requires CoreGuard or any paid tool — Activity Monitor and fs_usage ship with macOS, and the device layer uses the free, open-source smartmontools (brew install smartmontools).

  1. Write down the event. Record the time, workload, mounted volumes, and any active update, import, restore, build, export, VM, or sync. “Idle” is useful only if you say what idle meant.
  2. Preserve the volatile process view. In Activity Monitor, open Disk, choose View → All Processes, sort by Bytes Written, and record the leading names and values. If a helper is prominent, check View → All Processes, Hierarchically. Take a second reading after a known interval so you have deltas.
  3. Start a device bracket. Install smartmontools if needed (brew install smartmontools), run diskutil list internal physical, then smartctl -a disk0 with the actual internal identifier. Save the raw Data Units Written value and timestamp. If the event was underway, note that this brackets only its remainder.
  4. Capture broadly. Run sudo fs_usage -w -f filesys while the behavior occurs or while you deliberately reproduce it. Keep the first pass broad — but note fs_usage excludes Terminal and the shells (sh, zsh, tcsh) by default, so pass the exact command name or PID if you suspect a shell-driven writer. Stop with Control-C, or use a bounded -t capture redirected to a file. If you are bracketing the device counter (Layer 3), redirect that file to an external volume — writing it to the internal SSD adds to the same Data Units Written you are measuring.
  5. Find writes, then recover paths. Look for write and writev from the climbing process, read B= as requested bytes, and connect descriptor-only writes to nearby path-bearing events for the same process and descriptor. Keep rename, truncate, unlink, and synchronization calls in context.
  6. Narrow after evidence points somewhere. Rerun fs_usage with the exact name or PID. Reproduce the action only if it is safe and ordinary. If the filtered trace is quiet while the broad trace shows a helper, follow the helper.
  7. Close the bracket. Record Activity Monitor and smartctl -a disk0 again. Calculate process and Data Units Written deltas separately. Compare timing and direction; do not expect byte equality. If the trace was saved to the internal SSD, subtract its file size or treat the device delta as a contaminated upper bound.
  8. Repeat comparable conditions. One matched window is correlation. The same action, process or helper, path family, and device response recurring outside the old baseline makes attribution stronger. Address that specific app behavior or report it with a sanitized trace; do not make caches read-only, disable a macOS service, or delete files merely because their paths appeared.
THE JOIN IS TEMPORAL, NOT BYTE-EQUALITY workload changed process counter began climbing fs_usage showed calls to a path family device host-write counter advanced in the wider window ✓ supports: “this app behavior is associated with these host writes” ✗ does NOT support: “this Activity Monitor number = NAND wear”
Correlate onset, path, workload, and repetition across time. Byte-for-byte equality between the layers is never the admission test.

When the name is kernel_task — or just a helper

Process names are accounting endpoints. Sometimes they are the human cause; sometimes they are not.

A browser helper can write a cache or database even though a tab or extension triggered the work. An XPC helper can persist data for a client. A system daemon can own the file operation after an app hands it work. Activity Monitor is not wrong: it named the charged process. It simply did not identify who requested the higher-level behavior.

Treat kernel_task as “kernel-side work was accounted here,” not “the kernel decided to wear the SSD.” Look for paths and operations in fs_usage, the parent/child view where it applies, the preceding workload, and repetition. If those do not identify an origin, leave the cause unknown. (Why high kernel_task is usually protective is its own story: kernel_task and what it’s really doing.)

Paths are context, not magic attribution. An app-container path is strong evidence; a shared database, system cache, swap file, or filesystem metadata path may reflect work initiated elsewhere. The defensible sentence is often “helper X repeatedly wrote path family Y while action Z ran,” not “app Z physically wrote exactly N bytes to NAND.”

Delayed writeback breaks neat timestamp matching

The filesystem-call and device layers need not move in lockstep. macOS caches file data in a unified buffer cache, and writeback to storage can occur after the originating call — the macOS sync(2) manual describes scheduling modified buffers to be written to disk, i.e. a flush that happens later than the call itself. (Apple, sync(2) manual page)

The device counter may therefore keep rising after the visible app stops issuing obvious write calls. A process may exit before cached work reaches the controller, leaving later I/O without the name you expected.

Use a wider device window than the exact fs_usage burst and preserve timestamps. “Wider” is not a fixed number; include the observed tail on that Mac. Correlate onset, path, workload, and repetition. Exact byte equality is not the admission test.

Other activity still disturbs reconciliation. The internal device delta includes concurrent writers and filesystem work, while a process counter may include an external-volume write that disk0 never sees. Controller rounding adds another small boundary. Investigate missing writers; do not assign a remainder by intuition.

Preserve a transient event before it disappears

Keep a boring, raw evidence bundle:

  • Activity Monitor names, optional PIDs, and two Bytes Written readings with timestamps.
  • The unedited fs_usage capture, plus a separately redacted sharing copy.
  • Exact smartctl output before and after, including raw Data Units Written.
  • A one-line workload note and the macOS and app versions.
  • What happened before capture began, and whether it was reproduced.

Raw text is searchable; a screenshot preserves UI state and ranking. Keep both for a fleeting incident.

If the event does not recur, do not turn a plausible story into a fact. Preserve it as an observation. If it recurs, the safe next move is specific: update or report the implicated app behavior, change a setting only when evidence ties it to the writes, and verify the new baseline afterward. Attribution comes before intervention.

Where CoreGuard fits

The manual method works. Its weakness is timing: Activity Monitor is a snapshot, fs_usage must be running during the event, and SMART remembers bytes without the process. A transient writer can vanish before anyone opens Terminal.

CoreGuard automates the time-series observation without changing the system it observes. In Free, the abnormal-write warning is free, it names the process, and basic SSD danger warnings remain free. You do not pay to learn that a process’s write behavior became abnormal.

Pro adds the detailed per-app write timeline, longer history, and export. It also includes SSD-endurance detail — TBW, power-on hours, raw SMART fields, and the wear trend — while basic health and danger visibility stays free. The timeline is evidence, not a claim that an app counter equals NAND traffic.

CoreGuard observes. It never throttles, blocks, or modifies a writer. The whole manual workflow above stays complete and free — Pro only keeps that evidence after the transient moment passes, never gatekeeps the warning. CoreGuard isn’t out yet — get notified and grab it free at launch, or see what Pro adds.

Frequently asked questions

What is writing to disk on my Mac?

Open Activity Monitor's Disk tab, choose View then All Processes, sort by Bytes Written, and compare two readings. The process whose counter is climbing is a current suspect; the largest accumulated total may be idle now.

Which app is writing to my SSD on Mac?

Use Activity Monitor to name the process, then run sudo fs_usage -w -f filesys during the activity to reveal live calls and paths. A helper or kernel process may be the accounting endpoint, so correlate path and workload before blaming the visible app.

Why does Activity Monitor show high disk writes for an app?

Bytes Written is an accumulated OS process counter, not a current rate or physical NAND wear. Take a second reading after a known interval; only the delta describes what the process added then.

How do I monitor disk writes by process on Mac?

Activity Monitor provides per-process accumulated Bytes Written, while sudo fs_usage -w -f filesys streams current filesystem events by process. Use both because the first gives a counter and the second gives operations and paths.

What does fs_usage show on Mac?

fs_usage shows live filesystem calls, timestamps, file descriptors, requested byte counts, paths when available, errors, elapsed time, and process or thread. It requires elevated privileges and reports logical activity, not a lifetime total or physical NAND writes.

Catch the writer before it vanishes.

CoreGuard tracks disk writes per app over time. Free warns you when a write rate becomes abnormal and names the process; Pro keeps the detailed per-app write timeline, longer history, wear trend, and raw SMART so a transient writer doesn’t disappear before you open Terminal. It observes and explains — it never throttles a writer, cleans, optimizes, or claims to “extend” your SSD, and it’s not an antivirus.

launching soon · one-time purchase, not a subscription · 30-day money-back · local-only, zero telemetry

Sources & further reading

Related reading

All insights