A lightweight Linux daemon that mirrors NVMe disk activity to a chassis LED (e.g., ThinkPad power button LED) with minimal CPU overhead.
- Zero dependencies (except
libc) - Precise off-timer: dedicated timerfd for crisp LED edges
- Per-direction signaling: differentiate reads vs writes with distinct blink durations
- Config file support:
/etc/nvme-led-daemon.conffor easy tuning - Epoll + timerfd: efficient event loop, negligible CPU usage even at 8ms poll intervals
- Active-high/low support: works with various LED controller polarities
- Two NVMe modes:
io(I/O completions) orsectors(bytes transferred)
NVMe LED activity patterns (with my router in the background, guarded by Meowth):
nvme-led-demo.webm
If your browser doesn’t autoplay inline, click to open: nvme-led-demo.webm
Notes:
- Recorded with interval/read/write = 8/16/32 ms for frame alignment.
- LED patterns: short pulses (reads), longer pulses (writes), dense flicker (random I/O).
- Linux kernel with
epoll,timerfd, and sysfs LED class support - Rust toolchain (for building)
- Root or appropriate permissions to write to
/sys/class/leds/*/brightness
git clone https://github.com/scarlion1/nvme-led-daemon.git
cd nvme-led-daemon
cargo build --releasesudo cp target/release/nvme-led-daemon /usr/local/bin/sudo tee /etc/nvme-led-daemon.conf >/dev/null <<'EOF'
# NVMe LED Daemon Configuration
led_path = /sys/class/leds/tpacpi::power/brightness
nvme_path = /sys/block/nvme0n1/stat
interval_ms = 10
blink_ms = 10
read_blink_ms = 10
write_blink_ms = 20
active_high = true
nvme_mode = io
on_fields = both
quiet = false
EOFAdjust paths and values for your system:
led_path: find your LED withls /sys/class/leds/nvme_path: find your NVMe device withls /sys/block/nvme*active_high: set totrueif writing1turns LED on,falseif0turns it on
Some LEDs have kernel triggers that must be disabled:
echo none | sudo tee /sys/class/leds/tpacpi::power/triggersudo /usr/local/bin/nvme-led-daemonGenerate some disk activity (e.g., dd if=/dev/nvme0n1 of=/dev/null bs=1M count=100) and watch the LED blink.
sudo tee /etc/systemd/system/nvme-led.service >/dev/null <<'EOF'
[Unit]
Description=NVMe Power LED Activity Monitor
After=multi-user.target
[Service]
Type=simple
ExecStart=/usr/local/bin/nvme-led-daemon
Restart=on-failure
Nice=-10
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ReadOnlyDirectories=/
ReadWriteDirectories=/sys/class/leds/tpacpi::power
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now nvme-led.service
systemctl status nvme-led.service --no-pagersudo demos/test-nvme-led.shAll settings can be specified in /etc/nvme-led-daemon.conf (INI-style format) or overridden via CLI flags.
| Key | Type | Default | Description |
|---|---|---|---|
led_path |
string | /sys/class/leds/tpacpi::power/brightness |
Path to LED brightness file |
nvme_path |
string | /sys/block/nvme0n1/stat |
Path to NVMe stat file |
interval_ms |
u64 | 10 |
Poll interval in milliseconds |
blink_ms |
u64 | 10 |
Default LED on-duration in milliseconds |
read_blink_ms |
u64 | (optional) | Override blink duration for reads |
write_blink_ms |
u64 | (optional) | Override blink duration for writes |
active_high |
bool | false |
true if writing 1 turns LED on |
nvme_mode |
string | sectors |
io or sectors |
on_fields |
string | both |
reads, writes, or both |
quiet |
bool | false |
Suppress startup message |
--config PATH Load config from PATH (default: /etc/nvme-led-daemon.conf)
--led PATH LED brightness sysfs path
--nvme PATH NVMe stat file path
--interval-ms N Poll interval (ms)
--blink-ms N Default blink duration (ms)
--read-blink-ms N Blink duration for reads (ms)
--write-blink-ms N Blink duration for writes (ms)
--on-fields reads|writes|both
--nvme-mode io|sectors
--active-high LED is active-high
--quiet Suppress output
--help Show help
Balanced:
interval_ms = 10
read_blink_ms = 10
write_blink_ms = 20
nvme_mode = io
on_fields = bothReads only, short blink:
interval_ms = 10
read_blink_ms = 10
nvme_mode = io
on_fields = readsWrites only, longer blink:
interval_ms = 10
write_blink_ms = 30
nvme_mode = io
on_fields = writesVery responsive (more wakeups):
interval_ms = 6
read_blink_ms = 6
write_blink_ms = 12
nvme_mode = io
on_fields = bothAfter editing /etc/nvme-led-daemon.conf, restart the service:
sudo systemctl restart nvme-led.serviceA benchmarking script is included to measure the daemon's overhead under various polling intervals.
- Average CPU% (via pidstat)
- Context switches per second (ctxsw/s) as a proxy for wakeups/sec
- Kernel wakeups/sec via perf (if available)
# Requires root; the script will self-elevate
# Optional tools:
# Debian/Ubuntu: sudo apt install sysstat linux-tools-common linux-tools-$(uname -r)
# Fedora: sudo dnf install sysstat perf
chmod +x tools/bench-perf.sh
tools/bench-perf.shprofile interval theory_wps cpu idle% ctxsw/s idle perf_wake/s idle | cpu act% ctxsw/s act perf_wake/s act notes
------------ -------- ---------- ----------- ------------ ---------------- | ----------- ------------ ---------------- -----
ultra 6ms 166.7 2.67 171.47 0.00 | 1.93 171.40 0.00
responsive 8ms 125.0 2.00 128.33 0.00 | 1.40 128.60 0.13
balanced 10ms 100.0 1.00 102.27 0.07 | 0.80 102.00 0.00
...
- theory_wps ≈ 1000 / interval_ms (baseline wakeups/sec from the poll timer)
- ctxsw/s should closely match theory_wps at idle; small deltas are normal jitter
- perf wakeups may be near 0 on some kernels at idle; ctxsw/s is the more consistent proxy
CSV_OUT=bench.csv tools/bench-perf.shSee the tools README.
-
Check LED path and permissions:
ls -l /sys/class/leds/tpacpi::power/brightness echo 1 | sudo tee /sys/class/leds/tpacpi::power/brightness echo 0 | sudo tee /sys/class/leds/tpacpi::power/brightness
-
Disable LED trigger:
cat /sys/class/leds/tpacpi::power/trigger echo none | sudo tee /sys/class/leds/tpacpi::power/trigger
-
Check NVMe stat file:
cat /sys/block/nvme0n1/stat # Generate activity and check again dd if=/dev/nvme0n1 of=/dev/null bs=1M count=10 cat /sys/block/nvme0n1/stat -
Try the other nvme-mode:
- If
iodoesn't work, trysectors(or vice versa)
- If
-
Toggle active-high:
- Some LEDs are active-low; try flipping
active_highin the config
- Some LEDs are active-low; try flipping
-
Test with a known LED:
- Try capslock LED:
/sys/class/leds/input*::capslock/brightness
- Try capslock LED:
This is expected with NVMe—unlike old SATA disks, NVMe completes I/Os in large bursts with very low latency. The LED stays on because activity is nearly continuous. Lower blink_ms values (e.g., 6–10ms) can make individual pulses more visible during lighter workloads.
During testing on my 5-year-old T14 Gen 1 (Intel i5-10310U and 16GiB DDR4-3200) with the default settings (10ms interval), CPU usage was pretty low (<2%). If you see high usage:
- Increase
interval_ms(e.g., 20ms or even 50ms still has very good results) - Check for other system issues
- Epoll loop: waits on two timerfds (poll timer + off timer)
- Poll timer fires: reads
/sys/block/nvme*/stat, compares read/write counters - Activity detected: turns LED on immediately, arms off-timer for precise duration
- Off timer fires: turns LED off
- Repeat: minimal syscalls, low wakeups, efficient even at high poll rates
GPL-3.0-or-later
Developed with assistance from Claude Sonnet 4.5 and GPT-5 provided by Abacus.AI ChatLLM Teams, a super awesome and affordable service. Check them out and sign up with my referral link https://chatllm.abacus.ai/YwBngMwYCw and I'll give you a cookie.