How to find out why a Linux server rebooted

Introduction

uptime says four minutes. Nobody rebooted it.

Linux keeps enough evidence to answer this, in two places that disagree about what counts as a reboot. Here is how to read both, what each cause looks like, and why the machine sometimes cannot tell you.

Start with the boot list

last reboot
reboot   system boot  6.1.0-18-amd64   Mon Aug 17 12:13   still running
reboot   system boot  6.1.0-18-amd64   Fri Aug 14 03:02 - 12:12 (3+09:10)

Fast, and it tells you when but never why. last -x adds the shutdown and runlevel records, which is a first hint: a shutdown line before a reboot line means something asked politely.

The journal knows more:

journalctl --list-boots
IDX BOOT ID                          FIRST ENTRY                 LAST ENTRY
 -1 3d9f1c2b8a7e4f6091a2b3c4d5e6f708 Fri 2026-08-14 03:02:11 UTC Mon 2026-08-17 12:12:15 UTC
  0 7c1e5a4b2d3f489ab0c1d2e3f4a5b697 Mon 2026-08-17 12:13:09 UTC Mon 2026-08-17 12:41:02 UTC

Boot 0 is the current one. -1 is the one before it. The gap between -1’s last entry and 0’s first entry is how long the machine was down — 54 seconds above, which already rules out a power cut that needed someone to drive to the rack.

Read the end of the previous boot:

journalctl -b -1 -e

-e jumps to the last lines, which is where the answer lives.

What each cause looks like

Somebody asked for it

systemd-shutdown announces itself on the way out:

journalctl -b -1 -t systemd-shutdown
Shutting down.
Rebooting.

Those two lines mean systemd ran a shutdown sequence. Above them you will see units stopping in order. This is a clean reboot.

For who asked, check logind and the auth log:

journalctl -b -1 -u systemd-logind --since "10 min ago"
journalctl -b -1 -t sudo -g reboot

Automated reboots come from a package rather than a person. unattended-upgrades on Debian and Ubuntu, dnf-automatic with reboot = when-needed on Fedora and RHEL, kured on Kubernetes nodes. Search for the one you run:

journalctl -b -1 -u unattended-upgrades

The kernel ran out of memory

journalctl -k -b -1 -g "Out of memory"
Out of memory: Killed process 4211 (postgres) total-vm:8123456kB

The OOM killer usually kills one process rather than the machine, so this often explains a service dying, not a reboot. It explains a reboot when the process it killed was load-bearing enough for a watchdog or an operator to restart the host.

It panicked or locked up

The previous boot stops mid-sentence. No shutdown sequence, no systemd-shutdown lines, no warning. The last entry is whatever the machine happened to be saying.

Check the kernel ring buffer for the previous boot:

journalctl -k -b -1 -p err

A panic that got as far as printing leaves a trace here. A hard lockup, a hypervisor killing the guest, or the PSU giving up leave nothing, because nothing had a chance to write.

Distinguishing the two from the outside

What you see in boot -1 Most likely
systemd-shutdown lines, units stopping in order Clean reboot, asked for
Units stopping, then a hang, then nothing Shutdown that got stuck and was forced
Normal chatter, then nothing at all Panic, power loss or a hypervisor reset
Out of memory: Killed process shortly before the end Memory pressure, then something took the host down

The third row is the common one, and it is the one where the machine has told you everything it knows: nothing.

The catch: there may be no previous boot to read

Data from the specified boot (-1) is not available: No such boot ID in journal

That means the journal is volatile. journald wrote it to /run/log/journal, a tmpfs, and the reboot emptied it.

ls -d /var/log/journal 2>/dev/null || echo "volatile — the previous boot is gone"

The default Storage=auto keeps the journal on disk only if /var/log/journal already exists. Debian has shipped for years without that directory. So the first time many people go looking for why a box rebooted, the answer is that the evidence was in RAM.

Fix it before the next incident, not during it:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

Then cap it, or it will grow into the disk — see clearing journalctl logs and limiting journal disk usage.

Even a persistent journal loses the ending

Set Storage=persistent and you still do not get the last moment.

journald buffers. Entries reach page cache and then disk on the kernel’s schedule, not on journald’s. A machine that panics, loses power or is reset by its hypervisor stops between those two steps. The seconds you most want are the seconds that were still in flight.

And there is a worse version. If the reboot happened because the disk failed, the log describing the disk failure was being written to the disk that failed.

This is the whole argument for sending logs somewhere else. Not redundancy for its own sake — the machine you are asking is the machine that broke.

Read the previous boot from another machine

Ship the journal off the host and the last boot survives the host.

journalctl -o json --cursor-file=/var/lib/journal-ship.cursor \
  | curl -fsS -X POST --data-binary @- \
      "https://logs.example.com/api/v1/ingest_logs/YOUR-SOURCE-TOKEN"

Or install the CL Agent, which does the same every 30 seconds and ships host metrics on the same connection.

Every journal entry carries _BOOT_ID, so the boot list rebuilds itself with one query — across every host at once, which --list-boots cannot do:

SELECT json_extract(msg, '$._HOSTNAME') AS host,
       json_extract(msg, '$._BOOT_ID')  AS boot_id,
       datetime(MIN(CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER)) / 1000000, 'unixepoch') AS first_line,
       datetime(MAX(CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER)) / 1000000, 'unixepoch') AS last_line,
       COUNT(*) AS lines
FROM logs
WHERE json_valid(msg)
  AND json_extract(msg, '$._BOOT_ID') IS NOT NULL
GROUP BY host, boot_id
ORDER BY first_line DESC;

A new boot_id for a host is a reboot. The last_line of the previous one is the moment it went quiet.

Then ask whether it went quiet politely:

SELECT datetime(CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER) / 1000000, 'unixepoch') AS at,
       json_extract(msg, '$._HOSTNAME') AS host,
       json_extract(msg, '$.MESSAGE')   AS message
FROM logs
WHERE json_valid(msg)
  AND json_extract(msg, '$.SYSLOG_IDENTIFIER') = 'systemd-shutdown'
ORDER BY at DESC
LIMIT 50;

A host with a new boot and no systemd-shutdown line before it did not shut down. It stopped.

Both queries use journald’s own __REALTIME_TIMESTAMP, in microseconds, rather than the timestamp column. The column holds server receipt time, so a batch buffered during an outage arrives late and sorts as if it just happened. For reconstructing what occurred when, that is the wrong clock. Keep the json_valid(msg) guard too — json_extract raises an error on the first line that is not JSON.

Get told, instead of finding out

A reboot with no shutdown sequence is worth an alert. Two lines mark a boot: the kernel’s Linux version ..., and systemd’s Startup finished in ... when the boot target completes. The rule is “a boot happened, and nothing announced a shutdown first”:

SELECT 1
FROM logs AS boot
WHERE json_valid(boot.msg)
  AND (json_extract(boot.msg, '$.MESSAGE') LIKE 'Linux version %'
       OR json_extract(boot.msg, '$.MESSAGE') LIKE 'Startup finished in %')
  AND boot.timestamp > CAST(strftime('%s', 'now', '-10 minutes') AS INTEGER)
  AND NOT EXISTS (
        SELECT 1 FROM logs AS sd
        WHERE json_valid(sd.msg)
          AND json_extract(sd.msg, '$.SYSLOG_IDENTIFIER') = 'systemd-shutdown'
          AND json_extract(sd.msg, '$._HOSTNAME') = json_extract(boot.msg, '$._HOSTNAME')
          AND sd.timestamp BETWEEN boot.timestamp - 900 AND boot.timestamp
      )
LIMIT 1;

Create it on the Alerts page with Alert Condition set to alert when the query returns results, and Max Frequency at 15 minutes. Rules run every five minutes, so the -10 minutes window overlaps and a boot cannot fall between two evaluations.

SELECT 1 ... LIMIT 1 is deliberate. Central Logging only checks whether the result set has anything in it, so selecting columns is work nobody reads.

Whether the boot lines reach the server at all

This is the part that decides if the rule works for you, and it depends on how you ship.

The CL Agent seeds its cursor to the newest journal entry when it starts, and keeps that cursor in /tmp/clagent-cursor.txt. Both facts matter here. Anything logged before the agent process starts is skipped, and Linux version is emitted by the kernel long before any service runs. So the kernel marker will not arrive by that route. Startup finished is written when the boot target completes, which on most hosts is after the agent is already running — likely to arrive, but confirm it on your own host before trusting it.

A cron-based shipper catches both, as long as its cursor file is somewhere that survives a reboot:

journalctl -o json --cursor-file=/var/lib/journal-ship.cursor

/var/lib, not /tmp. The cursor points at the last entry sent, so the first run after a reboot replays everything since — including the kernel line. That needs a persistent journal, which brings you back to Storage=.

Two more honest caveats. The 15-minute lookback for the shutdown marker suits a host that reboots in under a quarter of an hour; a machine with a slow POST or a long fsck needs a wider window or it will alert on every clean reboot. And the rule reads the absence of shutdown lines, which is also what a dropped final batch looks like. Treat it as “go check this host”, not as proof.

If none of that fits your setup, use the blunter rule instead: alert when a host stops sending logs. It needs no boot markers, no persistent journal and no particular shipper, and a host that reboots goes quiet either way. It is covered in alerting on errors in your logs.

What shipping logs does not fix

It buys you the seconds that made it onto the network. It does not buy the ones that did not.

Ship every 30 seconds and a host that dies instantly takes up to 30 seconds of journal with it. A kernel panic that hangs the whole machine stops the shipper along with everything else. The cause is more often in the minutes before the end than in the final second, which is why this works in practice, but “more often” is the honest word.

If you need the true last gasp, that is a serial console or a hypervisor log, not a log shipper.

Where to go next

💌 Get notified on new features and updates

Only sent when a new version is released. Nothing else.