The task sounded like a five minute job: check whether a client server needs a reboot. Look at /var/run/reboot-required, answer yes or no. It took half a day, because almost every obvious-looking statement about needrestart turned out to be wrong once checked.

Here is what the tool actually does. Everything below was verified on a live machine: Ubuntu 22.04, running kernel 5.15.0-187, installed 5.15.0-190, a control panel and a dozen other people's services next to our site.

It does not reboot the system and cannot

The main misconception: if it talks about the kernel, surely it can reboot. It cannot. I checked three ways, because trusting memory on a production box is a bad idea.

The option list: the only action mode is -r, and all three of its values are about services - l (list), i (interactive), a (automatic). There is no «reboot the host» option. By the way, needrestart has no -h: usage is printed for any unknown flag.

The man page: the word reboot never appears, the kernel is mentioned once, as -k check for obsolete kernel, which is a check and nothing more.

The code: grepping /usr/share/perl5/NeedRestart found two hits, and both are about containers, not the host.

machined.pm:90   machinectl reboot <machine>
LXC.pm:115       lxc-stop --reboot --name <container>

So its role regarding the kernel is purely informational. It says so itself in its own dialog: «Restarting the system to load the new kernel will not be handled automatically».

There are two independent checks inside

The second thing worth understanding right away: the kernel and the services are checked separately and do not affect each other. You can run them one at a time.

$ sudo needrestart -k -b        # kernel only
NEEDRESTART-KCUR: 5.15.0-187-generic
NEEDRESTART-KEXP: 5.15.0-190-generic
NEEDRESTART-KSTA: 3

$ sudo needrestart -l -b        # libraries only
NEEDRESTART-SVC: exim4.service
NEEDRESTART-SVC: NetworkManager.service
NEEDRESTART-SVC: php8.1-fpm.service
...

KCUR is the running kernel, KEXP the expected one, KSTA the verdict: 1 fine, 2 ABI compatible, 3 reboot needed. The -b (batch) flag is mandatory when the command runs from a script or over ssh without a TTY: without it the program opens an interactive dialog and hangs waiting for input.

The consequence that usually raises questions: restarting daemons only removes the NEEDRESTART-SVC lines. The kernel block stays, and the «Pending kernel upgrade» screen keeps showing up until the new kernel actually boots.

Without sudo it silently walks away

Run as a regular user it looks broken:

$ needrestart
Scanning processes...
$

No list, no kernel, no dialog. The reason is simple: the library check reads other people's processes, the kernel check reads system state, and without root both return nothing. It can scan your own processes, of course, but there is nothing to restart there.

Interactive mode is more dangerous than it looks

The first screen without flags is whiptail, a text dialog with a border drawn in characters. No graphical mode involved, the program is simply waiting for a keypress. The «Pending kernel upgrade» screen is harmless, it is the visual form of the same KSTA: 3.

The next screen is the one to watch: a list of services with checkboxes, ticked by default. Confirming there immediately restarts the PHP-FPM pool, the mail daemon and NetworkManager on a live machine. The kernel still does not change. For a read-only look, -b is safer, and if you are already in the dialog, use Cancel or Esc.

The -p mode answers everything in one line

The most practical option, and the one we settled on. The format is not custom, it is the standard Nagios plugin one: STATUS - text | perfdata.

$ sudo needrestart -p
CRIT - Kernel: 5.15.0-187-generic!=5.15.0-190-generic (!!), Services: 6 (!),
Containers: none, Sessions: none|Kernel=2;0;;0;2 Services=6;;0;0 ...
exit code: 2

The marks in brackets are severity levels: no mark is OK, (!) is warning, (!!) is critical. The overall status comes from the worst item, not from the type of check. That is clearly visible on my own workstation, where the kernel was not part of the run:

CRIT - Services: 5 (!), Containers: none, Sessions: 1 (!!)

The services alone would have produced WARN, but a single stale session running outdated binaries pulled the whole answer up to CRIT. The exit codes come for free too: 0 clean, 1 services need a restart, 2 a reboot is needed. One command gives both a verdict for a human and a number for monitoring.

The manual alternative, uname -r plus dpkg | sort -V, looks simpler but is fragile: it trips over kernel flavours (-generic versus -virtual), over HWE kernels, and over the case where the newest installed kernel is not the one GRUB will pick. needrestart works out the expected kernel by itself.

apt calls it automatically, but only to print

A separate finding: needrestart is already wired into apt through /etc/apt/apt.conf.d/99needrestart, so it runs after every package operation, including nightly unattended-upgrades. In the nightly log it looks like it fixed everything:

Services to be restarted:
 systemctl restart exim4.service
 systemctl restart php8.1-fpm.service

Service restarts being deferred:
 systemctl restart NetworkManager.service

The key words are «to be restarted», not «Restarting». It prints the commands that ought to be run and exits. The reason is in the config: the $nrconf{restart} line in /etc/needrestart/needrestart.conf is commented out, so there is no active mode. You confirm this not from the log but with systemctl show -p ActiveEnterTimestamp: the unit start times had not moved.

The deferred category holds NetworkManager, which it deliberately refuses to restart. Sensible: on a machine you only reach over ssh, restarting the network daemon is a classic way to lose access.

reboot-required answers a different question

The familiar /var/run/reboot-required and its neighbour .pkgs are not an indicator but a log of reasons: which packages asked for a reboot during installation. As a freshness signal it fails on three counts. It is not only the kernel that writes there (libc6, dbus, systemd and udev add themselves too). It is not cleared by restarting services. And it is append only, so a package that asked for a reboot a week ago stays on the list.

It lives in /run, which is tmpfs, that is memory. Deleting it by hand is technically possible and pointless: it disappears on reboot anyway and comes back with the next kernel update. needrestart never touches that file, it computes its own answer independently.

What closes without a reboot

Exactly half the list: services holding deleted libraries. In our case those were libcurl, libpng16 and libpq, still open in php-fpm after the packages had been upgraded. A plain systemctl restart fixes that, with caveats: the PHP pool returns 502 during the restart, so a quiet hour is better, and the network daemon is best left to the reboot.

The kernel closes with nothing but a reboot. When it finally happened, the output became this:

$ sudo needrestart -p
OK - Kernel: 7.0.0-30-generic, Microcode: CURRENT, Services: none,
Containers: none, Sessions: none|Kernel=0;0;;0;2 Microcode=0;0;;0;1 ...

The Microcode line is not everywhere: it exists in needrestart 3.11 from a recent Ubuntu and is absent in 3.5 from 22.04. Worth remembering if you parse the output in a script.

Cheat sheet

sudo needrestart -p       # verdict plus exit code, fine for humans and monitors
sudo needrestart -k -b    # kernel only, machine readable
sudo needrestart -l -b    # only services on stale libraries
sudo needrestart -r l     # list, no dialog, no actions

And the rule all of this was written for: needrestart answers the question «what is stale», not «what to do». The decision about a reboot and its window stays with the person who has the rights and the responsibility for that machine.