journald and journalctl
journald is systemd's structured, indexed binary log store. journalctl queries it by unit (-u), priority (-p), boot (-b), and follows live with -f. Structured fields like _SYSTEMD_UNIT, PRIORITY, and _PID make filtering precise in a way plain-text logs never were.
A service fails. You run systemctl status nginx and get a four-line snippet ending with “failed.” The snippet isn’t enough — you need the 30 seconds before the crash. On a plain-text-log system you’d tail -f /var/log/nginx/error.log and hope the app wrote something useful. On a modern Linux box those logs are in journald — a binary, structured, indexed store that records every byte from every systemd unit since boot. journalctl is the query tool, and once you know four flags you can pull exactly the window you need, filtered by unit, priority, or time, in under ten seconds.
After this lesson you can query the systemd journal with journalctl, filter by service unit (-u), priority (-p), and boot (-b), follow live output (-f), and understand what structured fields like _SYSTEMD_UNIT, PRIORITY, and _PID are and why they matter.
journald captures everything systemd units write to stdout/stderr.
When you run a service under systemd, the unit’s stdout and stderr are piped directly into journald. You never need to configure a log file location — the journal is automatic.
# Show the last 50 lines of the entire journal (newest at bottom):
journalctl -n 50
# Show logs for a specific unit (the most important flag):
journalctl -u nginx.service
# Show logs for a unit from the current boot only:
journalctl -u nginx.service -b
# Combine: last 100 lines from nginx, current boot:
journalctl -u nginx.service -b -n 100The -u flag is the workhorse. Without it you get the full system journal — hundreds of lines per second on a busy box. With it you get exactly one service.
Filter by priority to cut through noise.
journald maps syslog severity levels to an integer 0–7. -p filters to that level and above (lower number = more severe).
# Priority levels (number = syslog severity):
# 0 emerg 1 alert 2 crit 3 err
# 4 warning 5 notice 6 info 7 debug
# Show only errors and worse for nginx since last boot:
journalctl -u nginx.service -b -p err
# Show warnings and worse, all units:
journalctl -b -p warning
# Show everything including debug (very verbose):
journalctl -u myapp.service -p debug
# Filter a range (e.g. warning through err):
journalctl -u myapp.service -p warning..errIn practice -p err is the first thing you run when a service misbehaves. It drops info and notice noise and shows only what went wrong.
Time filters: --since and --until.
Boot boundaries (-b) are convenient, but sometimes you need a specific window — say, the 5 minutes before an alert fired.
# Since a specific time:
journalctl -u postgres.service --since "2024-03-15 14:00:00"
# Time range:
journalctl -u postgres.service \
--since "2024-03-15 14:00:00" \
--until "2024-03-15 14:10:00"
# Relative time (last 30 minutes):
journalctl -u postgres.service --since "30 min ago"
# Boot offsets: -b 0 = current boot, -b -1 = previous boot, -b -2 = two boots ago:
journalctl -u postgres.service -b -1
# List available boots:
journalctl --list-boots--list-boots is underused. It shows every boot record with its index, so after a crash-reboot you can reach back to -b -1 and read the dying moments of the previous session.
Follow live output with -f.
-f tails the journal in real time — like tail -f but for structured logs.
# Follow all new journal entries live:
journalctl -f
# Follow only a specific unit live (the one you actually want):
journalctl -u nginx.service -f
# Follow + priority filter (only errors live):
journalctl -u myapp.service -f -p err
# Combined: follow errors from two units simultaneously:
journalctl -u nginx.service -u php-fpm.service -f -p errYou can combine -f with all the other flags. -f -p err during a deployment gives you a live stream of only fatal events without noise.
Structured fields: what makes journald different from plain-text logs.
Every journal entry is a key-value record, not a line of text. The binary format stores typed fields alongside the message. You can query on any field.
# Show verbose output with all fields for a unit:
journalctl -u nginx.service -o verbose | head -40
# Output includes:
# _SYSTEMD_UNIT=nginx.service
# PRIORITY=3
# _PID=1234
# _UID=0
# _COMM=nginx
# MESSAGE=connect() failed (111: Connection refused)...
# Query by a specific field value (e.g., only from PID 1234):
journalctl _PID=1234
# All entries from the www-data user:
journalctl _UID=33
# Multiple field filters (AND):
journalctl _SYSTEMD_UNIT=nginx.service _PID=1234
# Output formats: short (default), json, cat (message only):
journalctl -u nginx.service -b -p err -o json | head -5
journalctl -u nginx.service -b -o catKey structured fields: _SYSTEMD_UNIT (which unit), PRIORITY (0–7), _PID (process ID), _UID (user ID), _COMM (command name), MESSAGE (the actual log line). Filtering on _PID is something you literally cannot do with grep on a mixed log file.
Finding why nginx failed to start after a config change.
You edited /etc/nginx/sites-enabled/myapp.conf and reloaded nginx. It is now in a failed state.
# Step 1: check systemctl status for the recent snippet
systemctl status nginx.service
# ● nginx.service - A high performance web server
# Active: failed (Result: exit-code)
# Main PID: 5821 (code=exited, status=1/FAILURE)
# ...last 10 lines of journal...
# Step 2: get the full story from journald — current boot, errors only
journalctl -u nginx.service -b -p err
# Mar 15 15:32:11 host nginx[5821]: nginx: [emerg] unknown directive "proxy_cache_pat"
# Mar 15 15:32:11 host nginx[5821]: nginx: configuration file test failed
# Typo found: "proxy_cache_pat" instead of "proxy_cache_path"
# Step 3: also check info-level for the full config test output:
journalctl -u nginx.service -b -p info -n 30
# Mar 15 15:32:11 host nginx[5821]: nginx: [emerg] unknown directive "proxy_cache_pat"
# Mar 15 15:32:11 host nginx[5821]: /etc/nginx/sites-enabled/myapp.conf:8
# Line 8 shown — exact file and line number
# Step 4: fix and restart, then follow to confirm clean start:
# (edit the config, then:)
sudo systemctl restart nginx && journalctl -u nginx.service -f -n 5
# Mar 15 15:35:02 host nginx[5890]: start worker processes
# Mar 15 15:35:02 host systemd[1]: Started A high performance web server.▸Why this works
Why binary + structured beats plain text. Plain-text log files require you to know which file the app writes to, parse timestamps with grep, and hope the format is consistent. journald stores every entry with guaranteed typed fields (_PID, _UID, _SYSTEMD_UNIT, PRIORITY) regardless of what the app writes in its message. You can filter on any field without grep. The binary format also enables fast indexed seeks — --since "5 min ago" is a binary search, not a full file scan. The tradeoff: you cannot cat the journal, and you need journalctl or the journal API to read it.
▸Common mistake
macOS has no journald. macOS uses the Unified Logging system (log show, log stream, log collect). log show --predicate 'process == "nginx"' --last 5m is the rough equivalent of journalctl -u nginx -p err --since "5 min ago". The concepts (structured fields, binary store, indexed queries) map directly, but the commands are completely different. Do not expect journalctl on macOS — it does not exist.
You want to see only error-level and above messages from the sshd service since the last reboot, and you want to follow them live as new ones arrive. Which journalctl invocation is correct?
journald is systemd’s structured, binary log store. Every systemd unit’s stdout/stderr is captured automatically — no log file path to configure. journalctl -u <unit> filters to one service. -b restricts to the current boot; -b -1 reaches the previous boot (useful after a crash-reboot). -p err shows only errors and worse, cutting through noise. -f follows live output. -o verbose exposes typed structured fields: _SYSTEMD_UNIT, PRIORITY, _PID, _UID, MESSAGE — these enable precise filtering impossible with plain-text grep. The binary format is the reason you cannot cat the journal, and the reason --since is a fast indexed seek rather than a file scan.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.