journalctl -f is the direct equivalent of tail -f, and journalctl -n 50 is tail -n 50. The reason to use them instead of the real tail is that the journal is structured, so you can narrow to one service with -u, to a time window with --since, and to a severity with -p — filters a flat text file cannot offer. journalctl -u myapp -f is the command worth memorising.
There is no journalctl tail subcommand, which is why the search exists. The functionality is there under different flag names, and it does more than the tool it replaces.
Table of contents
- The direct translations
- Narrowing to a unit, which is the real win
- Time ranges
- Filtering by priority
- Output formats worth knowing
- Grep, follow, and the buffering trap
- Disk usage and retention
- When the log is on a machine you should not be SSHing into
- How this fits the rest of the stack
- FAQ
The direct translations
# tail -f -> follow the journal live
journalctl -f
# tail -n 50 -> last 50 entries
journalctl -n 50
# tail -f on one service
journalctl -u nginx -f
# Last 100 lines of one service, then follow
journalctl -u myapp -n 100 -f
That last one is the everyday command. -n before -f gives you context for what just happened before the live stream starts, which is almost always what you want when something is failing right now.
-f implies a default of the last 10 lines. -n with no number defaults to 10 as well; -n 0 follows with no history at all.
Narrowing to a unit, which is the real win
-u takes a systemd unit name. The .service suffix is optional.
journalctl -u myapp.service -f
journalctl -u myapp -f # same thing
# Several units at once
journalctl -u myapp -u postgresql -f
# Wildcards work
journalctl -u 'myapp-*' -f
Multiple -u flags interleave the units in time order, which is how you catch “the app failed because the database restarted” — the causal pair you would never spot in two separate terminal windows.
Time ranges
--since and --until accept both absolute timestamps and plain English, and this is where the journal comfortably beats a text file.
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp --since "2026-08-13 09:00" --until "2026-08-13 10:30"
journalctl -u myapp --since yesterday
journalctl -u myapp --since today -p err
# Since the last boot
journalctl -u myapp -b
# The boot before this one
journalctl -u myapp -b -1
-b is the one to reach for after an unexpected reboot: journalctl -b -1 -p err shows the errors from the boot that ended, which is where the reason usually is.
Filtering by priority
-p takes a syslog severity and shows that level and everything more severe.
0emerg,1alert,2crit,3err,4warning,5notice,6info,7debug
# Errors and worse, live
journalctl -p err -f
# Warnings and worse for one service since boot
journalctl -u myapp -p warning -b
# Exactly one level, not "and above"
journalctl -u myapp -p warning..warning
journalctl -p err -b is a good first command on a machine behaving oddly — it is the whole system’s complaints since boot, in one screen, in time order.
Output formats worth knowing
# Everything the journal recorded about each entry
journalctl -u myapp -n 5 -o verbose
# JSON, one object per line -- pipe to jq
journalctl -u myapp -o json-lines | jq 'select(.PRIORITY == "3")'
# Just the message, no timestamp or hostname
journalctl -u myapp -o cat
# Wall-clock timestamps rather than relative
journalctl -u myapp -o short-iso
-o cat is useful when the application already timestamps its own lines and you do not want two. -o json-lines is what makes the journal genuinely structured rather than text pretending to be structured — you can filter on fields the message body never contained.
Add --no-pager in scripts, or journalctl will hand output to less and hang waiting for a key that will never be pressed.
Grep, follow, and the buffering trap
journalctl has built-in matching, which is faster than piping to grep because it filters at the journal level.
# Built-in regex match
journalctl -u myapp -g 'timeout|refused' -f
# Piping to grep needs line buffering, or you see nothing for ages
journalctl -u myapp -f | grep --line-buffered ERROR
That --line-buffered matters. Without it, grep buffers its output in 4KB blocks and a quiet log appears frozen — the classic “my follow command stopped working” that is actually a buffer waiting to fill.
Disk usage and retention
The journal is not infinite, and by default on some distributions it is not even persistent across reboots.
# How much space is it using?
journalctl --disk-usage
# Trim to 500M
sudo journalctl --vacuum-size=500M
# Trim to the last 14 days
sudo journalctl --vacuum-time=14d
For persistence across reboots, Storage=persistent in /etc/systemd/journald.conf plus a systemctl restart systemd-journald. If journalctl -b -1 says there is no such boot, this is why — the previous boot’s logs were in memory and are gone.
When the log is on a machine you should not be SSHing into
All of the above assumes you can get a shell on the box. That assumption is doing a lot of work, and it gets weaker as the number of instances grows.
Following a unit on one host tells you what that host saw. When three instances sit behind a load balancer, journalctl -f on one of them is a sample, and the failing request is a two-in-three chance of being somewhere else.
Logs aggregated per deploy — build output and runtime output for the version actually serving traffic, without choosing a host first — is the version of this that scales. On RunxBuild that view is the default, which turns “which box was it” into a question you no longer have to ask.
How this fits the rest of the stack
journalctl -u <unit> -n 100 -f is the command to keep. Add --since for a window, -p err for severity, -b -1 for the boot that just ended, and -o json-lines | jq when you need real structure. Remember --line-buffered if you pipe to grep, and --no-pager in scripts.
When following logs means picking a host first, the logs are in the wrong place. If you are working out what a setup with per-deploy logs costs, the RunxBuild hosting calculator itemises service, database, storage, and bandwidth separately rather than quoting one number.
Useful related references:
- Linux tail Command: -f for Live Logs, -n for Last N Lines
- kubectl logs: Tail, Follow, and Multi-Container Pods
- How to Troubleshoot a Load Balancer: 502s, Health Checks, and the Long Tail
- Services on RunxBuild
FAQ
What is the journalctl equivalent of tail -f?
journalctl -f. Add -u <unit> to follow one service and -n <count> to print history before the live stream begins. journalctl -u myapp -n 100 -f gives you the last hundred lines then follows, which is the practical everyday form.
How do I show the last N lines with journalctl?
journalctl -n 50 prints the most recent 50 entries. Combine with -u to scope it to a service. With no number, -n defaults to 10, and -n 0 combined with -f follows without printing any history first.
How do I see journalctl logs for a specific time range?
Use --since and --until, which accept both absolute timestamps and relative phrases: journalctl --since "1 hour ago", or --since "2026-08-13 09:00" --until "2026-08-13 10:30". --since yesterday and --since today also work.
Why does journalctl -f piped to grep show nothing?
grep buffers output when writing to a pipe rather than a terminal, so matches sit unseen until the buffer fills. Add --line-buffered to grep, or use journalctl’s own -g pattern flag, which filters inside the journal and is faster anyway.
Why are my journalctl logs gone after a reboot?
The journal defaults to volatile in-memory storage on some distributions, so it is discarded at shutdown. Set Storage=persistent in /etc/systemd/journald.conf, create /var/log/journal if it does not exist, and restart systemd-journald. After that journalctl -b -1 can show the previous boot.