Skip to content
Go back

NVMe SMART: Reading Wear Before It Dies

By KingPin 13 min read
NVMe SMART: Reading Wear Before It Dies
Contents

Your SSD Does Not Warn You, It Just Stops

A spinning disk clicks, grinds, and gives you a week of dread. An NVMe drive runs at full speed on Tuesday and shows up as a 0 GB device on Wednesday. If you only ever look at the green “PASSED” line, you will find out at 2 AM, usually from your users.

The fix is to read three numbers: Percentage Used, Available Spare, and Media and Data Integrity Errors. The drive tracks all of them in its SMART health log, and they move well before a wear-out failure. Controller and firmware deaths still come without warning, so keep backups. The overall health verdict only flips once the drive crosses a hard limit.

This is the NVMe-specific deep dive on interpreting those numbers. If you need the smartd and email setup, that lives in the smartmontools guide. If you want to cut writes in the first place, see reducing SSD write wear.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/linux/nvme-smart-wear-leveling

How a Controller Spreads the Damage

NAND flash wears out by erasing. Each block survives a limited number of program/erase (P/E) cycles, and consumer TLC and QLC survive fewer than the MLC and SLC NAND in older drives. A block that gets erased constantly dies first, and a drive that kills its first block on day 40 is a bad drive.

So the controller never lets the OS pick physical locations. The Flash Translation Layer (FTL) keeps a map from logical block addresses to physical pages. When you overwrite a file, the controller writes the new data to a fresh page somewhere else and marks the old page stale. Nothing is rewritten in place.

Two tricks keep wear even:

The drive also holds back spare blocks that the OS never sees. This is over-provisioning. When a block goes bad, the controller retires it and swaps in a spare. That reserve is what the Available Spare field reports.

One more number matters: write amplification. Garbage collection and static wear leveling copy data around internally, so the NAND absorbs more writes than your host sent. A drive with a write amplification factor of 3 burns through its P/E budget three times faster than the host write counter suggests. The controller knows this, and the host does not, which is why the drive’s own wear estimate beats your arithmetic later.

Meet the Health Log

Every NVMe controller exposes a SMART / Health Information log, Log Identifier 02h. Both tools read the same 512 byte page. Here it is through nvme-cli 2.16, from a 1 TB OEM drive with 23,000 power-on hours:

Terminal window
sudo nvme smart-log /dev/nvme0
Smart Log for NVME device:nvme0 namespace-id:ffffffff
critical_warning : 0
temperature : 45 °C (318 K, 113 °F)
available_spare : 100%
available_spare_threshold : 10%
percentage_used : 14%
endurance group critical warning summary: 0
Data Units Read : 829049945 (424.47 TB)
Data Units Written : 115340906 (59.05 TB)
host_read_commands : 4943652991
host_write_commands : 4056000107
controller_busy_time : 97900
power_cycles : 118
power_on_hours : 23368
unsafe_shutdowns : 50
media_errors : 0
num_err_log_entries : 0
Warning Temperature Time : 0
Critical Composite Temperature Time : 0

(I trimmed the thermal management counters at the bottom. They were all zero.)

Now each field, ranked by how much it should worry you.

Percentage Used: the odometer

percentage_used is the controller’s estimate of how much of the drive’s rated life you consumed. The spec calls it vendor specific. 100 means you hit the rated endurance. The drive usually keeps working past that point. The value can climb past 100 and is capped at 255. This drive says 14, so it has used roughly one seventh of its budget after 23,368 hours.

Treat it as a trend. Log it monthly. A drive that goes from 14 to 15 in a year is fine. A drive that goes from 14 to 40 in a month has a workload problem, and the fix is to find the process doing the writing.

Available Spare and its threshold

available_spare is a normalized percentage of the reserve blocks left, starting at 100. available_spare_threshold is the vendor’s line in the sand. Here it is 10%. When the spare drops below the threshold, the controller sets bit 0 of Critical Warning.

On a healthy drive this field sits at 100 for most of its life. When it starts dropping, blocks are dying faster than the drive expected. Copy your data off while you still have an easy choice.

Media and Data Integrity Errors

media_errors counts times the controller detected an unrecovered data integrity error: an uncorrectable ECC failure, a failed CRC, a failed LBA tag check. The correct value is 0. Any nonzero value means the controller hit data it could not recover, usually a NAND read it could not correct.

One error can be a fluke. A number that climbs is the “back up now” signal, and it deserves more attention than anything else on this page.

Critical Warning: the bitfield

critical_warning is a single byte. Each bit is a separate alarm:

BitMeaning
0Available spare fell below the threshold
1Temperature is above an over-temperature or under-temperature threshold
2NVM subsystem reliability degraded (significant media errors or an internal error)
3Media placed in read-only mode
4Volatile memory backup device failed
5Persistent memory region became read-only or unreliable (newer spec revisions)

Anything other than 0 needs a human. Bit 3 is the nasty one: the drive protects your data by refusing writes, and your services start throwing I/O errors. Bit 5 only applies to drives with a persistent memory region, which you will not find in a typical homelab box.

Data units, TBW math

Data Units Written counts in units of 1000 sectors of 512 bytes, so one unit is 512,000 bytes. The controller reports the number in thousands and rounds up. The conversion is one line of shell:

Terminal window
echo "scale=2; 115340906 * 512000 / 10^12" | bc
# 59.05

That matches the 59.05 TB nvme-cli printed. Now compare to the spec sheet. Say the vendor rates a drive at 600 TBW (an example, not this drive). After 59 TB written you used about 9.8% of the rated endurance.

Percentage Used and TBW often disagree, and the drive’s number wins. Percentage Used is the controller’s estimate, and write amplification and NAND wear differ by workload. This OEM drive has no rated TBW I could verify, so use the controller’s own figure: 59.05 TB at 14% implies about 420 TB to reach 100% (59.05 / 0.14 = 421.8). That projection assumes your future writes look like your past writes, and 14% is a coarse integer, so treat it as a ballpark.

Temperature, power cycles, and unsafe shutdowns

temperature is the composite temperature. The drive reports it in Kelvin, and both tools convert it to Celsius. Warning Temperature Time and Critical Composite Temperature Time count minutes spent above the warning and critical thresholds. Zero is what you want. A drive that spent 400 minutes past critical had a bad airflow day.

unsafe_shutdowns counts power losses without a shutdown notification. This drive has 50 across 118 power cycles. Each one risks the FTL map and in-flight writes, but a modern drive with power-loss protection or a well-behaved firmware handles them. A number that grows fast on a machine with no UPS means you should fix your power.

The field everyone misreads

num_err_log_entries counts entries in the Error Information log over the life of the controller. It sounds like “errors.” It is not. Failed admin commands, a probe for a feature the drive does not support, or an nvme command the drive rejects can all add an entry. A nonzero value on a healthy drive is common noise. This drive happens to show 0, and plenty of perfectly good drives do not.

Do not alert on it. Alert on media_errors, which counts data the drive could not recover.

smartctl vs nvme-cli: Which One

Both tools read log 02h. They differ in what they wrap around it.

smartctl (smartmontools 7.5) is the better choice for monitoring. It outputs JSON with -j, feeds smartd, and has exporters for Prometheus. The same fields in JSON, from the real drive:

Terminal window
sudo smartctl -j -a /dev/nvme0 | jq '.nvme_smart_health_information_log | {critical_warning, temperature, available_spare, available_spare_threshold, percentage_used, data_units_read, data_units_written, media_errors, num_err_log_entries, power_on_hours, unsafe_shutdowns}'
{
"critical_warning": 0,
"temperature": 45,
"available_spare": 100,
"available_spare_threshold": 10,
"percentage_used": 14,
"data_units_read": 829049945,
"data_units_written": 115340906,
"media_errors": 0,
"num_err_log_entries": 0,
"power_on_hours": 23368,
"unsafe_shutdowns": 50
}

nvme-cli is the better choice for digging. It exposes the other log pages (error log, endurance group log), plus vendor plugins that read extra counters smartctl does not know. Examples that exist in the nvme-cli source: nvme intel smart-log-add and nvme wdc vs-smart-add-log. It prints JSON too, with nvme smart-log /dev/nvme0 -o json.

One gotcha with nvme-cli: in version 3.0 and later, nvme smart-log, nvme self-test-log, and nvme id-ctrl are deprecated aliases for nvme log smart, nvme log self-test, and nvme id ctrl. Each prints a deprecation warning on stderr. The tested output in this post is from 2.16, and the latest release is newer (3.1 as of October 2026). Scripts should switch to the new names.

My rule: smartctl in cron and smartd, nvme-cli when something looks wrong and you want the raw log pages.

The PASSED trap

smartctl -H on an NVMe drive prints PASSED. On the real drive the JSON says "smart_status": {"passed": true, "nvme": {"value": 0}}. The smartctl man page says NVMe status comes from the Critical Warning byte. So PASSED means critical_warning == 0 and nothing else.

That makes PASSED a lagging indicator. Percentage Used can be at 95 and media errors can be climbing while the drive still says PASSED. Do not build a monitoring check around it.

Self-Tests: Ask the Drive to Check Itself

NVMe 1.3 added an optional Device Self-test command. Check support first. The JSON shows it:

Terminal window
sudo smartctl -j -a /dev/nvme0 | jq .nvme_optional_admin_commands.self_test
# true

With nvme-cli you read the OACS field (nvme id-ctrl /dev/nvme0 | grep oacs), where bit 4 is Device Self-test. If the drive supports it, run a short test:

Terminal window
sudo nvme device-self-test /dev/nvme0 --self-test-code=1 --wait
sudo nvme self-test-log /dev/nvme0

Code 1h is short, 2h is extended, fh aborts a running test. The short flag form is -s 1. smartctl can do the same with smartctl -t short /dev/nvme0, but the 7.5 man page still labels NVMe self-tests as an experimental feature that was new in 7.4. The spec targets two minutes or less for a short test. Run one monthly and look at the self-test log for failures.

A Check You Can Cron

Doing this by eye does not scale past one machine. The companion script reads smartctl -j -a, converts data units to TB, decodes the Critical Warning bits, and exits 0 (OK), 1 (WARN), 2 (CRIT), or 3 if it cannot read the drive:

nvme-wear-check.py
tb = log["data_units_written"] * 512000 / 1e12 # 1 unit = 1000 x 512 bytes
used, spare, thr = log["percentage_used"], log["available_spare"], log["available_spare_threshold"]
media, cw = log["media_errors"], log["critical_warning"]
flags = [n for i, n in enumerate(CRIT_BITS) if cw >> i & 1]
if cw or spare < thr:
print("CRIT: back up now"); return 2
if used >= args.warn_used or spare - thr <= args.spare_margin or media > 0:
print("WARN: plan a replacement"); return 1

smartctl returns a bitmask exit status, so a nonzero code does not mean the JSON is bad. The script parses the output either way. It also keeps no history, so it WARNs on any nonzero media_errors. It cannot tell you the count went up. Compare runs in your monitoring system for that. Run it against the sample file:

Terminal window
./nvme-wear-check.py --json sample-smartctl.json --rated-tbw 600
written: 59.05 TB
rated TBW: 9.8% of 600 TB written
used: 14% (controller estimate)
spare: 100% (threshold 10%)
media errs: 0
crit warn: 0x00 none
OK

If you already run Prometheus, a smartctl exporter exposes these fields as metrics and you can alert on percentage_used and media_errors there. The smartmontools post covers the exporter setup, so I will not repeat it.

What to Alert On

Short list, ranked:

  1. critical_warning not zero: page yourself.
  2. media_errors increasing between runs: back up now.
  3. available_spare within 10 points of the threshold: order a replacement.
  4. percentage_used at 80 or more: plan the swap.
  5. Everything else: graph it, do not alert on it.

Buying a new drive at 80% is cheaper than a restore at 2 AM. Your 2 AM self will thank you.

Common Questions

How accurate is NVMe Percentage Used?

NVMe Percentage Used is a vendor estimate, so accuracy varies by manufacturer. The controller counts its own NAND wear, including write amplification, which host-side TBW math cannot see. Trust the trend more than the absolute number, and log it monthly so you notice a sudden jump.

Is 100% Percentage Used on an NVMe drive dead?

No. A drive at 100% Percentage Used has reached its rated endurance and usually still works. The value can go past 100 and caps at 255. Many drives keep working beyond 100%, but the rated endurance is spent and failure risk rises. Treat 100% as the point to replace the drive on your schedule.

Why does smartctl say PASSED on a dying SSD?

smartctl reports PASSED for NVMe when the Critical Warning byte equals zero. That byte only flips after the drive crosses a hard limit, such as spare below threshold or read-only media. A drive can show climbing media errors and high Percentage Used while still reporting PASSED, so read the individual fields.

Does nvme-cli need root?

Yes. Reading the SMART log sends an admin passthrough command, and the Linux kernel checks for CAP_SYS_ADMIN on that command no matter who owns /dev/nvme0. A udev group rule does not help. Run sudo nvme smart-log /dev/nvme0. smartctl has the same requirement. Only the composite temperature is readable without root, through the hwmon sensor.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Rootless Podman Quadlets vs Docker
Next Post
Immich Backups That Outlive Immich

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts