journalctl Is a Database and You Are Using It Like a Text File
The standard incident ritual is journalctl -u myservice | grep error | tail. It works, which is why nobody upgrades it, and it loses, which is why incidents take longer than they should. The journal is not a text file. It is a structured store with fields for unit, boot, priority, timestamp and process, and every grep against the raw stream throws those fields away before reading them.
This was systemd 255 on Ubuntu 24.04. The query flags have been stable for years.
Query by unit, not by grep
The unit field means you never need to grep for the service name:
journalctl -u myservice --since "1 hour ago"
More than one unit, comma separated:
journalctl -u myservice -u myworker --since today
This is faster and, more importantly, exact. A grep for the service name also matches every line that merely mentions it, including other services logging about it, which is noise at the worst moment.
The boot dimension
"Did this happen before or after the restart" is one of the most common incident questions, and the journal answers it natively because it indexes by boot:
journalctl -b # this boot
journalctl -b -1 # the previous boot
journalctl --list-boots # all of them
Grepping cannot tell you which boot a line belongs to. The -b flag makes the before and after of a crash restart trivially separable, which is usually the whole diagnosis.
Priority as a filter, not an eyestrain exercise
The journal stores syslog priority. Filtering to warnings and above removes the routine chatter:
journalctl -u myservice -p warning --since today
The inverse, showing only what a service logged at debug while everything else stays normal, is how you read a noisy service without drowning. Priority filtering is the journal doing the grep for you, on a field rather than on text.
Following the right thing
-f follows everything, which on a busy host is a firehose. Following a unit, or a priority, or both, turns it into a usable stream:
journalctl -u myservice -f
During a reproduction, following the unit at debug priority while you trigger the bug is the closest thing to a live debugger most services get.
The time window is the query
The single highest value habit is bounding every query in time. An incident has a window, and the journal can slice it:
journalctl --since "2026-09-14 02:10:00" --until "2026-09-14 02:25:00"
A bounded window across all units shows you the neighbourhood of the failure, the kernel, the neighbour services, the timers, which is exactly what a single unit grep hides. Failures are rarely alone in the journal, and the window query is how you see the company they kept.
Output formats for machines
When you need to process rather than read, the JSON output gives you the structured fields directly:
journalctl -u myservice -o json --since "1 hour ago" | jq 'select(.PRIORITY=="3")'
Now you are querying a database, and jq is your SQL. Extracting a field across events, counting by message, correlating by a request id you logged as a structured field, all become one liners instead of fragile text pipelines.
If you log structured fields yourself, key value pairs in the log line become journal fields when logged via the native API, and the queries get even sharper. This is the payoff of structured logging and what to log, realised at query time.
Persistence and the gotchas
Two gotchas cost people at the worst moment. First, if /var/log/journal does not exist, the journal is volatile and dies with the boot, so your previous boot queries return nothing. Check with journalctl --list-boots, and if history matters, make the journal persistent.
Second, the journal is shared and can be rate limited under heavy logging, so a service that floods can have its own messages dropped, which means absence in the journal is not always absence of the event. When a gap looks suspicious, check the drop counters before concluding silence.
The rule
The journal already parsed your logs into fields. Query the fields, unit, boot, priority, time window, and reserve text search for the content inside a well bounded slice, not as the first move.
Every incident question you have been answering with a grep pipeline is a query the journal can answer directly, faster and without the noise. The difference between grepping a text file and querying a database is the difference between reading and asking, and the journal has been waiting to be asked.
Correlating one request across units
The journal's real payoff arrives when a request crosses services. If every service logs a request id, and you log it as a recognisable token, the whole journey is one query:
journalctl --since "10 minutes ago" -o short | grep 'req_id=ab12'
With structured output it is cleaner, selecting on the field and ordering by time, which reconstructs the request's path through the proxy, the API and the worker in the order it happened. This is the log side of the trace, assembled from the store you already pay for, and it turns "which service dropped it" from a guessing game into a read.
The prerequisite is the boring one: the id must be logged by every hop, in a consistent format, which is a contract, not a convention. When one service omits it, that service becomes the black spot in the journey, and the black spot is itself the answer to where observability died.
The rule
The journal already parsed your logs into fields. Query the fields, unit, boot, priority, time window, and reserve text search for the content inside a well bounded slice, not as the first move.