•
6 min read

The UPS has no timer

Table of Contents

Runtime bar: 2 hours 15 minutes at about 30 W. A ghosted mark at minute 5 where the timer would have fired, and the real trigger in the last ten minutes.

The whole argument in one bar. Rendered from the UPS’s own readings, September 4, 2026.

A hard power cut in the middle of a write is what corrupts a chainstate. Our node, an Umbrel Home running Bitcoin Knots, now sits on a CyberPower CP1500PFCLCD, and the UPS exists to prevent that one failure and nothing else. It does not keep us online: the fiber terminal has no battery, so a real outage kills the internet within seconds whatever the node does. It does not save power. It buys the node a clean shutdown instead of a truncated one.

The obvious design

Wire the UPS to the node over USB, run NUT, and when the line drops, start a timer. If the power is still out N seconds later, shut down. Cancel the timer if the line comes back. That was the first draft, with N at 300 seconds to conserve battery.

It is wrong, and not because of the number. Shutting down early only pays if the machine comes back on its own when power returns. That is the one thing we have not verified, and until it is, an early shutdown risks a node that stays dark until someone walks over and presses the button. A conserved battery is worth nothing if the outcome is a node dark for a week. An outage that ends at minute 55 cost nothing if we never acted.

So the timer is gone, and the NUT scheduler that would have run it is not installed. The only trigger is the UPS’s own low-battery signal. The UPS computes remaining runtime from the load it is actually carrying, so no guessed number is load-bearing anywhere. A blink, a five-minute cut and a two-hour cut all end the same way: nothing happened, everything carried on.

Measured, not assumed

Read from the UPS on September 4, 2026, with the node and the router on it:

Runtime at real load8125 s, about 2 h 15 m
Load3% of 1000 W, roughly 30 W for both boxes
Low-battery trigger10% charge, factory, about 10 to 13 minutes at this load
Driverusbhid-ups, NUT 2.8.1

The unit also reports 11425 s when it rounds the load down to 0%. Treat 2 h 15 m as the number and the three-hour reading as optimistic.

Ten minutes is a short window for a clean shutdown, so we tried to raise it. Both thresholds are effectively read-only on this unit. upsrw returns OK and the hardware ignores it. Setting the low-runtime threshold to 1200 seconds came back as 176, worse than the factory 300, and was restored. Setting the low-charge threshold to 20% silently stayed at 10. Read the value back after a poll cycle; never trust the OK.

Because the threshold could not be raised, the other side shrank to fit. Docker’s default stop timeout is 10 seconds, which is not enough for bitcoind to flush its chainstate, and that truncated write is the exact corruption this setup exists to prevent. The shutdown path stops the Knots container by hand with a 90-second timeout, then halts the host. Ninety rather than 300 because the whole sequence has to fit inside a window we cannot widen. Same rule as the timer: when a knob will not turn, delete or shrink, do not tune.

What we tested

Unit 1’s wall plug, pulled for 16 seconds at 22:37 UTC on September 4. Status went from on-line to on-battery within about a second, input voltage dropped to 0, and it recovered to on-line, charging. Nothing shut down.

Both boxes are genuinely on the battery bank, proved better than by a load reading: a poller was reaching the node over SSH every two seconds throughout the outage, over a path that runs through the router, and did not drop a single sample.

What we have not tested

Whether the machine comes back by itself. Three links, and the third is the doubt.

  1. The driver’s kill-power flag is off, so NUT will not tell the UPS to cut its own output.
  2. The UPS re-energizes on mains return by design, with a 30-second start delay.
  3. The Umbrel Home firmware has to be set to power on when AC is applied. Umbrel hid the HDMI port inside the case, so reaching that setting means opening the box and finding a monitor.

This risk is not new and NUT did not add it. Before any of this, a long enough outage still ended with that box unpowered, and link 3 still decided whether it came back. What changed is that it now goes down clean instead of mid-write. Untested as of September 5, 2026; this line gets updated when it is.

What is configured

On the umbrelOS host itself, not in a container. NUT from Debian, MODE=standalone, one usbhid-ups device with port = auto, upsmon as primary. No upssched. The shutdown command is a script that runs docker stop -t 90 on the Knots container, then shutdown -h now. Enough to reproduce with the NUT documentation. The router shares the battery outlets because it corrupted its own configuration on an abrupt cut the day before, and a battery is the only mitigation a router has.

Check it is alive with three commands, none of which need root:

upsc unit1 ups.status       # OL = on line, OB = on battery, LB = low battery
upsc unit1 battery.runtime  # seconds left at current load
systemctl is-active nut-driver@unit1 nut-server nut-monitor

Healthy is OL, a runtime around 8000, and active three times.

Discuss this post on X

Saved you an hour? Tip the node and the clankers.