Guides
Guide

journalctl: How to Read, Follow, and Filter systemd Logs

By Sazabi Team

Last verified on Nov 8, 2026.

journalctl reads the systemd journal, the binary log store that systemd-journald fills from services, the kernel, and syslog clients. The two commands you will use most are journalctl -u <unit> to read one service and journalctl -f to follow new entries as they arrive. The rest of this page covers the filters that make those useful, JSON output, and how to keep the journal from filling the disk.

Read and Follow Logs

Run journalctl with no arguments to page through everything the journal holds, oldest first.

journalctl                    # all entries, oldest first
journalctl -n 50              # the 50 most recent entries
journalctl -r                 # newest first
journalctl -f                 # most recent entries, then stream new ones
journalctl -e                 # open the pager at the end
journalctl --no-pager | tail  # plain output for pipes and scripts

-f (--follow) prints the most recent entries and keeps printing as new ones are appended, until you press Ctrl-C. -n limits output to a count and defaults to 10 when you give no number. Following also implies -n, which is why journalctl -f starts with a short backlog rather than the whole journal.

The pager is less, and long lines are truncated to the screen width. Use the arrow keys to see the rest, or pass --no-pager when piping to grep or jq.

Filter by Unit

-u (--unit) shows messages from a systemd unit, and accepts patterns that are matched against unit names found in the journal.

journalctl -u nginx.service
journalctl -fu nginx          # follow one service
journalctl -u 'postgresql*'   # pattern match
journalctl -u nginx -u php8.3-fpm   # two units, interleaved
journalctl --user-unit=my-app.service   # a user session unit
journalctl -t sshd            # by syslog identifier instead of unit
journalctl -k                 # kernel messages for the current boot

Use -t when a program logs under a syslog identifier that is not a unit, such as a cron job. -k (--dmesg) adds the match _TRANSPORT=kernel and implies the current boot.

Filter by Time and Boot

--since and --until (short forms -S and -U) take timestamps in the format 2012-10-30 18:17:16. If you drop the time, 00:00:00 is assumed. You can also use yesterday, today, tomorrow, now, or a relative time prefixed with - or +.

journalctl -u nginx --since "2026-11-08 09:00" --until "2026-11-08 10:00"
journalctl --since yesterday --until today
journalctl --since=-2h       # the last two hours
journalctl --list-boots       # numbered boots with first and last timestamps
journalctl -b                 # current boot
journalctl -b -1              # previous boot

The previous-boot query only works if the journal survived the reboot. See the storage section below. --output=short-full prints timestamps in exactly the format --since accepts, so you can copy a time from one line and paste it into the next query.

Filter by Priority

-p (--priority) uses the syslog levels. Give a single level and you get that level and everything more severe. Give a range and you get both ends and everything between.

NumberNameTypical meaning
0emergSystem unusable
1alertAct immediately
2critCritical condition
3errError
4warningWarning
5noticeNormal but significant
6infoInformational
7debugDebug detail
journalctl -p err             # levels 0 to 3
journalctl -p warning..crit   # levels 2 to 4
journalctl -b -p err -u nginx # errors from one unit since boot

Priority is a field the sender sets, so a service that logs everything at info will not show up under -p err no matter how bad the line is. See log levels for how to choose them in your own code.

Search Message Text

-g (--grep) takes a PCRE regular expression and matches it against the MESSAGE= field. If the pattern is all lowercase, matching is case insensitive, otherwise it is case sensitive. --case-sensitive overrides that either way.

journalctl -u sshd -g 'failed password'
journalctl -b -g 'out of memory|oom-kill'
journalctl -u nginx -g 'upstream timed out' --since=-1h

When you combine -g with -n, --reverse is implied, so -n 20 -g timeout gives the 20 newest matches, newest first.

Match on Journal Fields

The journal is structured. Each entry has fields such as _SYSTEMD_UNIT, _PID, PRIORITY, and _COMM, and you can match on them directly.

journalctl _SYSTEMD_UNIT=nginx.service _PID=1234     # both must match
journalctl _SYSTEMD_UNIT=nginx.service _SYSTEMD_UNIT=php8.3-fpm.service   # either
journalctl _SYSTEMD_UNIT=nginx.service + _SYSTEMD_UNIT=sshd.service       # explicit OR
journalctl -N                 # list every field name in use
journalctl -F _COMM           # list every value of a field

The man page states the rules: two different fields must both match, two matches on the same field are an OR, and a + separator forces a logical OR between groups. This is a sharper tool than grep because you filter on the field instead of text that happens to be in the message.

Output as JSON

-o json prints one JSON object per line, which pairs well with jq and with the idea of structured logging.

journalctl -u nginx -o json --since=-10min | jq -r '[.__REALTIME_TIMESTAMP, .PRIORITY, .MESSAGE] | @tsv'
journalctl -u nginx -o json-pretty -n 1
journalctl -u nginx -o cat    # message text only, no metadata
journalctl -u nginx -o json --output-fields=MESSAGE,PRIORITY -n 5

Field values are mostly strings. Fields over 4096 bytes come out as null unless you add --all, and a field with non-printable or non-UTF-8 bytes is encoded as an array of byte values. Test your jq filters against a real entry first, since a message that is null or an array will break a naive .MESSAGE | test(...).

Make the Journal Survive Reboots

Where the journal lives is controlled by Storage= in /etc/systemd/journald.conf. With Storage=auto, logs are stored on disk under /var/log/journal only if that directory exists, and otherwise in memory under /run/log/journal. Memory storage is lost at reboot, so check which one your host uses.

sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
# or set it explicitly:
# /etc/systemd/journald.conf.d/persistent.conf
# [Journal]
# Storage=persistent

Rate limits matter here as well. journald limits each service, by default to 10000 messages in 30 seconds. Past that, it drops messages until the interval ends and writes a message saying how many were dropped. If a noisy service loses lines, look for that notice before you suspect the shipper.

Control Disk Usage

journalctl --disk-usage               # active plus archived files
sudo journalctl --vacuum-size=500M    # shrink archived files to 500M
sudo journalctl --vacuum-time=2weeks  # delete archived files older than 2 weeks
sudo journalctl --vacuum-files=10     # keep at most 10 archived files

Vacuuming only removes archived files, so --disk-usage can stay above the limit you asked for because active files count too. For a permanent limit, set these in journald.conf:

[Journal]
SystemMaxUse=1G
SystemKeepFree=2G
MaxRetentionSec=1month

SystemMaxUse= and SystemKeepFree= default to 10% and 15% of the filesystem, each capped at 4G, and journald respects both and uses the smaller. MaxRetentionSec= defaults to 0, which turns time-based deletion off. The man page says size-based limits are normally enough, and time-based deletion makes sense when you need to enforce a retention policy. If you also keep plain files, compare with logrotate, which solves the same problem for files in /var/log.

Read Journals From Other Places

Journal files from a dead host or a mounted volume can be read without importing them.

journalctl -D /mnt/rescue/var/log/journal -b -p err
journalctl --file /tmp/system.journal -u nginx

Root and members of the systemd-journal, adm, and wheel groups can read all journal files. Everyone else sees only their own user journal, which is the usual reason journalctl seems empty for a non-root account. Add the user to systemd-journal instead of using sudo everywhere.

Forward the Journal to a Central Store

Reading logs host by host does not scale. Log forwarding agents read the journal for you. Fluent Bit has a systemd input that supports systemd_filter for field matches, a db file that records the journal cursor so a restart does not replay or skip entries, and read_from_tail to skip history on first start. See the Fluent Bit integration for the other half of the pipeline.

pipeline:
  inputs:
    - name: systemd
      tag: host.*
      systemd_filter: _SYSTEMD_UNIT=nginx.service
      db: /var/lib/fluent-bit/journal.db
      read_from_tail: true

Sending Journal Logs to Sazabi

Sazabi, an AI-native observability and security platform, documents a Linux auditd log source that reads /var/log/audit/audit.log plus journald entries with SYSLOG_FACILITY 4 (auth) and 10 (authpriv). It uses a Fluent Bit or OpenTelemetry Collector agent on each host and sends the records over OTLP/HTTPS. That covers sshd, sudo, PAM, and logind messages. For other units, Sazabi's Fluent Bit page shows the opentelemetry output block, which you wire to whichever inputs you already run, including a systemd input like the one above. The OpenTelemetry Collector is the documented alternative agent. Once records arrive, you can ask about them in plain language from the CLI, for example sazabi logs ask "errors from nginx in the last hour", as described in the log search docs. Failed logins are covered in the failed SSH login attempts guide, and the access anomalies guide shows what to watch for after you have the data.

Frequently Asked Questions

How do I follow logs with journalctl?

Run journalctl -f. It prints the most recent entries and then streams new ones until you press Ctrl-C. Add -u followed by a unit name, for example journalctl -fu nginx, to follow one service.

How do I see logs for one service since a given time?

Use -u for the unit and --since for the start. For example, journalctl -u nginx --since "2026-11-08 09:00" --until "2026-11-08 10:00" shows one hour of that unit's entries.

Why does journalctl show nothing from before the last reboot?

The journal is probably stored only in memory. With Storage=auto, logs go to disk only if /var/log/journal exists. Create that directory or set Storage=persistent in journald.conf, then restart systemd-journald.

How do I limit how much disk space the journal uses?

Set SystemMaxUse= in journald.conf for a permanent cap, or run journalctl --vacuum-size=500M to delete the oldest archived files now.

Sources

  1. journalctl flags: -f, -u, -p (levels 0 to 7 and FROM..TO ranges), -b, -g, -S/-U, -o json, -k, --disk-usage, --vacuum-size, and the AND/OR rules for matches (accessed Nov 8, 2026)
  2. Storage= values, SystemMaxUse= defaults capped at 4G, MaxRetentionSec=, and per-service rate limiting defaulting to 10000 messages in 30s (accessed Nov 8, 2026)
  3. The Fluent Bit systemd input reads journald with systemd_filter, a db cursor file, and read_from_tail (accessed Nov 8, 2026)
  4. Sazabi reads journald auth messages (SYSLOG_FACILITY 4 and 10) and auditd records through Fluent Bit or the OpenTelemetry Collector (accessed Nov 8, 2026)
  5. sazabi logs ask answers plain-language questions about stored logs from the CLI (accessed Nov 8, 2026)
  6. Fluent Bit v2.0 or later forwards logs to Sazabi with the built-in opentelemetry output over OTLP/HTTPS (accessed Nov 8, 2026)