You know roughly when it broke. You want the journal for those ten minutes and nothing else.
--since and --until do that. Both are easy to use and easy to get subtly
wrong, and the wrong version returns fewer lines rather than an error. This
guide covers the syntax, the three traps, and what to do when the window you
want is older than the journal.
journalctl --since "2026-08-28 14:00" --until "2026-08-28 14:10"
Entries print oldest first. Add -u nginx to narrow it to one unit, and
--no-pager if you are piping it somewhere.
Anything prefixed with - counts backwards from now.
journalctl --since "-1h" # the last hour
journalctl --since "-30min" # the last thirty minutes
journalctl --since "-2 days" # the last two days
The spelled-out form works too, and reads better in a script someone else will have to maintain:
journalctl --since "1 hour ago"
journalctl --since "10 min ago" --until "5 min ago"
That last one is the shape you want during an incident. A window with both ends
pinned stays the same window while you keep re-running it, which a bare
--since "-10min" does not.
The full form is YYYY-MM-DD HH:MM:SS. You can leave parts off, and what
journalctl fills in is where people get caught.
| What you type | What journalctl uses |
|---|---|
2026-08-28 14:37:02 |
exactly that |
2026-08-28 14:37 |
14:37:00 — seconds default to :00 |
2026-08-28 |
2026-08-28 00:00:00 — midnight |
14:37 |
14:37:00 today |
A bare date means midnight at the start of that day. So this command does not do what it looks like it does:
journalctl --since 2026-08-27 --until 2026-08-28
It gives you all of the 27th and none of the 28th. --until 2026-08-28 stops
at 00:00:00 on the 28th, before anything happened. If you want both days,
name the end explicitly:
journalctl --since 2026-08-27 --until "2026-08-28 23:59:59"
journalctl understands three words, and they are all midnight:
yesterday — 00:00:00 of the day before todaytoday — 00:00:00 of todaytomorrow — 00:00:00 of the day after todaynow is the exception. It means the current time.
So journalctl --since yesterday gives you yesterday and today, which
surprises people who expected one day. For only yesterday, pin both ends:
journalctl --since yesterday --until today
An absolute time with no timezone on it is read in the machine’s local time. That is fine on one server and a problem across several, because a window that means 14:00 on a box in Berlin means something else on a box in Chicago.
Append the timezone to say what you mean:
journalctl --since "2026-08-28 14:00 UTC" --until "2026-08-28 14:10 UTC"
The literal string UTC works, and so does an IANA name like
Europe/Berlin. Add --utc to print the results in UTC as well, so the
timestamps you read match the window you asked for:
journalctl --since "2026-08-28 14:00 UTC" --utc -o short-iso
-o short-iso gives sortable RFC 3339 timestamps. The default output format
omits the year, which is not what you want in a window that spans a New Year or
in anything you paste into a ticket.
--since and --until are just two more matches. Everything ANDs together.
journalctl -u nginx --since "-1h" -p err
journalctl -k --since "2026-08-28 03:00" --until "2026-08-28 04:00"
journalctl -u postgresql --since today -g "deadlock"
One combination behaves differently from the rest. -n limits the count
after the window is applied, and it takes the newest entries in that window,
not the oldest:
journalctl --since "-1h" -n 20 # the newest 20 of the last hour
journalctl --since "-1h" -n +20 # the oldest 20 of the last hour
The + prefix is the whole difference. Without an argument, -n defaults to
10.
Two things quietly limit how far back you can go.
The journal may not be persistent. If /var/log/journal does not exist,
systemd’s default Storage=auto stores the journal in /run/log/journal,
which is memory. A reboot empties it. --since "3 days ago" on that machine
returns whatever has happened since the last boot and no error at all.
Check what you have:
journalctl --list-boots
One line means one boot’s worth of history. To make it persistent:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
The journal is capped even when it is persistent. systemd vacuums old
entries to stay under SystemMaxUse=, and it does not ask first. See
clearing journalctl logs and limiting journal disk usage
for the caps and how to read the current usage.
When you are chasing something around a restart, boot numbers beat clock times.
journalctl --list-boots # -1, 0 and so on, with the times of each
journalctl -b # this boot
journalctl -b -1 # the previous boot
journalctl -b -1 -p err # the errors that ended it
Finding out why a Linux server rebooted goes through that in full, including what to do when there is no previous boot listed.
This is where the single-host commands stop.
An incident at 14:03 rarely stays on one box. You want the same ten minutes from the proxy, the app and the database, lined up in one list. On separate hosts that means three SSH sessions, three journals and three local clocks that may not agree — and the machine you most want to read is sometimes the one that stopped responding.
Shipping the journals to one place turns the window into a WHERE clause.
Central Logging stores each line in SQLite, so the window becomes SQL. There are two clocks available and they are not the same clock.
timestamp is when the server received the line. It is a Unix timestamp in
seconds, and it is the right column for “what arrived recently”:
SELECT datetime(timestamp, 'unixepoch') AS received,
source,
msg
FROM logs
WHERE timestamp BETWEEN CAST(strftime('%s', '2026-08-28 14:00:00') AS INTEGER)
AND CAST(strftime('%s', '2026-08-28 14:10:00') AS INTEGER)
ORDER BY timestamp;
datetime(..., 'unixepoch') renders UTC. Add 'localtime' if you would rather
read it in the server’s zone:
SELECT datetime(timestamp, 'unixepoch', 'localtime') AS received FROM logs;
__REALTIME_TIMESTAMP is when the event happened. journald records it, the
agent ships it, and it survives the trip. Use it when the two could differ —
after an outage a backlog arrives all at once and sorts by receipt time as
though it just occurred. It is in microseconds, so divide by a million:
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 CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER) / 1000000
BETWEEN CAST(strftime('%s', '2026-08-28 14:00:00') AS INTEGER)
AND CAST(strftime('%s', '2026-08-28 14:10:00') AS INTEGER)
ORDER BY at;
That query is one line longer than it looks like it needs to be, for two reasons worth knowing.
json_extract raises an error the first time it meets a line that is not JSON.
One plain-text line in the source is enough to stop the whole query. WHERE json_valid(msg) AND ... protects the terms after it, because SQLite
short-circuits AND.
It does not protect the SELECT list. Those expressions run on every row
the WHERE admits, so the guard has to eliminate the bad rows, not just sit in
front of them.
strftime('%s', ...) returns text, not a number. SQLite sorts every text value
above every integer regardless of what the text says, so comparing an extracted
number against an uncast strftime matches nothing — and raises no error:
-- returns zero rows, silently
WHERE CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER) / 1000000
BETWEEN strftime('%s', 'now', '-1 hour') AND strftime('%s', 'now')
The version against the timestamp column happens to work without the cast,
because that column has integer affinity and SQLite converts the text operand
to match. An extracted value has no column affinity to lean on.
Cast both sides every time. It costs nothing and removes the question.
Once the window is in SQL, narrowing it is arithmetic:
SELECT strftime('%Y-%m-%d %H:00', timestamp, 'unixepoch') AS hour,
count(*) AS lines
FROM logs
GROUP BY hour
ORDER BY hour;
That is usually how you find the ten minutes you should have been asking about.
-p err often finds nothing.-g, and searching every host at once.--cursor-file for repeated exports.-u in depth, and what to check when a unit prints nothing.💌 Get notified on new features and updates