
Linux command: tail
Something is wrong on a server, and the first thing anybody types is tail -f on a log file. It is probably the most-used command in system administration, and almost everybody uses it in a way that will silently stop working at midnight tonight, when the log rotates. This article is about the ten-line default, the plus sign that changes what the number means, and the one letter that decides whether you are still watching your log tomorrow morning.
1. The Basics
head and tail are a matched pair with one job between them: show you part of a file without showing you all of it. head takes the start, tail takes the end. They share almost all of their options, so learning one teaches you most of the other.
1.1 The Simplest Possible Use
Both print ten lines if you do not say otherwise. That number is not configurable and it has been ten for decades:
$ seq 1 20 > n.txt
$ head n.txt
1 2 3 4 5 6 7 8 9 10 # shown on one line here to save space
$ tail n.txt
11 12 13 14 15 16 17 18 19 20
With no file at all they read standard input, which is what makes them useful in a pipeline:
$ ps aux --sort=-%mem | head -n 5
$ dmesg | tail -n 20
1.2 Choosing How Much
Two options control the amount, and they are the same on both commands:
| Option | Short for | Meaning |
|---|---|---|
-n NUM |
number of lines | Show NUM lines instead of 10 |
-c NUM |
characters (bytes) | Show NUM bytes instead of lines |
$ head -n 3 n.txt 1 2 3
$ tail -n 3 n.txt 18 19 20
$ printf 'abcdefghij' > b.bin
$ head -c 4 b.bin abcd
$ tail -c 4 b.bin ghij
-c takes the multiplier suffixes you would expect, and it is worth knowing that K means 1024 while kB means 1000:
$ tail -c 1K big.txt | wc -c
1024
1.3 The Plus Sign Changes What the Number Means
This is the part people skip, and it doubles what both commands can do. A plain number counts from the near end. A number with a sign counts from the other end.
| Form | Means | On a 20-line file |
|---|---|---|
tail -n 3 |
The last 3 lines | 18 19 20 |
tail -n +18 |
Everything from line 18 onwards | 18 19 20 |
head -n 3 |
The first 3 lines | 1 2 3 |
head -n -18 |
Everything except the last 18 | 1 2 |
Those four give the same answers on a 20-line file, which is a coincidence of the arithmetic and a good way to see the difference. On a file whose length you do not know, they are completely different questions. tail -n 3 always gives you three lines. tail -n +18 gives you however many lines exist from 18 onwards, which might be three or three million.
Back to topThe right mental model: without a sign you are asking "how many lines do you want?". With a sign you are asking "where do you want to start?". Almost every clever
headortailone-liner is really about that distinction.
2. Where the Names Come From
The names are literal. A file has a head and a tail the way a queue or a list does, and the commands print one or the other. There is no acronym and no hidden joke.
What is worth knowing is the asymmetry underneath. These two look like siblings written together, and they were not. They come from different Unix lineages, they arrived a year apart, and only one of them ever grew the feature that made it famous. tail got -f. head never needed it: a file does not grow at the front.
That one option is the reason tail is a household name and head is not. The vocabulary around it is small and worth having straight:
-f, --follow keep the file open and print new data as it is appended
-F shorthand for --follow=name --retry (see section 6)
descriptor follow THIS file, even if it gets renamed (the -f default)
name follow whatever has THIS name, reopening as needed
--retry keep trying to open a file that is missing or unreadable
rotation when a log is renamed or truncated and a fresh one begins
inotify the kernel interface that tells tail the moment a file changes
Two of those matter more than the rest. Descriptor versus name is the whole of section 6, and it is the difference between a tail -f that survives the night and one that does not. And inotify is why modern tail -f reacts instantly instead of waking up once a second to look.
3. A Short History
The two commands did not arrive together. head came from Berkeley and tail came from Bell Labs, within about a year of each other, which is why they are so similar and yet were never quite designed as a pair.
| Era | Milestone |
|---|---|
| 1978 | head first appears in 1BSD, the first Berkeley Software Distribution. |
| 1979 | tail first appears in Version 7 Unix at Bell Labs. |
| 1980s onwards | tail -f spreads, and log watching becomes a habit rather than a technique. |
| 1992 | POSIX standardises both. The syntax it blesses is -n NUM, not the older -NUM. |
| 1990s | GNU reimplements both for coreutils, adding -F, --retry, --pid and the negative and plus forms of the count. |
| 2000s onwards | Linux gains inotify, and tail -f quietly stops polling and starts reacting instantly. |
The manual pages on any Linux machine still credit the people who did the GNU work:
$ man tail | grep -A2 AUTHOR
Written by Paul Rubin, David MacKenzie, Ian Lance Taylor, and Jim Meyering.
$ man head | grep -A2 AUTHOR
Written by David MacKenzie and Jim Meyering.
The POSIX line in that table has a living consequence. The old syntax was a bare number, and it still works because breaking it would break thirty years of scripts:
$ tail -5 n.txt # works, but it is the obsolete form
16 17 18 19 20
$ tail -n 5 n.txt # the POSIX form: use this one
16 17 18 19 20
Write -n 5. The short form is fine when you are typing at a prompt, but it is not portable, it cannot express -n +5 without ambiguity, and section 7.3 shows the case where it produces a genuinely confusing result.
Portability is worth a minute of your time, because this article is going to tell you to use -F and you may be writing for a container image rather than a full distribution. BusyBox, which is what you get inside most minimal images, implements more than people expect:
| Feature | GNU coreutils | BusyBox 1.38 |
|---|---|---|
-n, -c, -q, -v, -f, -s |
Yes | Yes |
-F |
Yes | Yes ("same as -f, but keep retrying") |
-n +N, -n -N, -c -N |
Yes | Yes, identical results |
--pid |
Yes | No |
--retry, --follow=name |
Yes | No: no long options at all |
$ docker run --rm busybox tail --pid=1 -f /tmp/a
tail: unrecognized option '--pid=1'
$ docker run --rm busybox tail --retry /tmp/a
tail: unrecognized option '--retry'
So the headline advice survives a minimal image: -F works. What does not travel is --pid, --retry and the long-option spellings, so a script that might run inside a container should stick to the short forms. macOS and the BSDs are a third dialect again; their manuals document -F as well, but not the GNU long options. Check with tail --help or the local manual page rather than assuming.
4. Simple Use Cases
4.1 Looking at the Ends of Things
The everyday uses need no explanation, only the habit:
$ head -n 20 /etc/services # what is at the top of this file?
$ tail -n 50 /var/log/nginx/error.log # what just went wrong?
$ head -n 1 *.csv # the header row of every file
$ tail -n 1 /etc/passwd # the most recently added account
4.2 Skipping a Header Row
This is the single most useful thing the plus sign does, and it comes up constantly with CSV files and with commands that print a title line:
$ tail -n +2 data.csv # everything EXCEPT the header
$ ps aux | tail -n +2 # the processes, without the column titles
$ df -h | tail -n +2 | sort -k5 -hr # sort by use%, header out of the way
Read -n +2 as "start at line 2". It is the idiomatic way to drop a header in a shell pipeline, and it is much safer than grep -v against the header text, which will also delete any data row that happens to match.
4.3 Several Files at Once
Give either command more than one file and it labels them for you:
$ tail -n 1 f1.txt f2.txt
==> f1.txt <==
a2
==> f2.txt <==
b2
Two options control those labels, and both are occasionally exactly what you need:
$ tail -q -n 1 f1.txt f2.txt # -q for quiet: no headers, just the lines
a2
b2
$ tail -v -n 1 f1.txt # -v for verbose: force a header on ONE file
==> f1.txt <==
a2
-q is the one you want when you are feeding the result into another command, because those ==> lines are data as far as the next program is concerned. A quick way to get the last line of every log in a directory:
$ tail -q -n 1 /var/log/*.log | sort
4.4 Bytes Instead of Lines
-c is the right tool when the file has no useful lines, or when you care about size rather than structure:
$ head -c 512 disk.img | xxd | head -n 4 # look at a boot sector
$ head -c 2M huge.log > sample.log # grab a manageable sample
$ tail -c 100 file.bin | xxd # the last 100 bytes
head -c is the polite way to sample a very large or binary file, because it stops reading as soon as it has what it needs. Section 7.1 measures exactly how much that saves.
One caution on text. Bytes are not characters, and -c counts bytes. Cut in the wrong place and you slice a multibyte character in half:
$ printf 'aeiou \xc3\xa9\xc3\xa8\xc3\xaa done\n' > u.txt # three 2-byte chars
$ tail -c 12 u.txt | xxd
00000000: c3a9 c3a8 c3aa 2064 6f6e 650a # lands on a boundary: fine
$ tail -c 11 u.txt | xxd
00000000: a9c3 a8c3 aa20 646f 6e65 0a # starts mid-character
$ tail -c 11 u.txt | iconv -f UTF-8 -t UTF-8
iconv: illegal input sequence at position 0
A lone continuation byte is not valid UTF-8, so the first character comes out as a replacement glyph or as nothing at all. For anything with accents, emoji or non-Latin text, count lines rather than bytes.
Back to top5. Moderate Use Cases
5.1 Cutting Out the Middle
Neither command can show you lines 8 to 12 on its own. Together they can, in two different ways:
$ head -n 12 n.txt | tail -n 5 # take the first 12, then the last 5 of those
8 9 10 11 12
$ tail -n +8 n.txt | head -n 5 # start at line 8, then take 5
8 9 10 11 12
They give the same answer, but they are not equally good. The second form starts at line 8 and stops after five more, so it never reads the rest of the file. On a small file this is irrelevant. On a multi-gigabyte log it is the difference between instant and slow, and it is worth making the second form your habit.
sed -n '8,12p' and awk 'NR>=8 && NR<=12' do the same job. They are more flexible and they are slower, because they parse every line. Use them when the range depends on the content; use head and tail when it depends only on the position.
5.2 Following a Log
-f (short for follow) is why tail is famous. It prints the last ten lines and then stays open, printing new lines as they are written:
$ tail -f /var/log/nginx/access.log
$ tail -n 100 -f /var/log/syslog # start with more context
$ tail -f /var/log/mail.log # Ctrl-C to stop
Combining -n with -f is a small habit worth forming. The default ten lines is rarely enough context when you are trying to understand what led up to the thing you are waiting for.
5.3 Following Several Logs at Once
tail -f accepts multiple files, and it prints a header every time the output switches from one to another:
$ tail -f m1.log m2.log
==> m1.log <==
from one
==> m2.log <==
from two
==> m1.log <==
one again
For a web server that logs access and errors separately, this is genuinely useful:
$ tail -f /var/log/nginx/access.log /var/log/nginx/error.log
The headers only appear when the source changes, so a burst of lines from one file stays together and stays readable.
5.4 Piping to grep, and Why Nothing Appears
Everyone writes this, and on a quiet log everyone is then baffled:
$ tail -f app.log | grep ERROR
# ... an error is written to the log ...
# ... and nothing appears. For a long time.
Nothing is broken. When grep writes to a terminal it flushes every line, but when it writes to a pipe or a file it buffers around 4 KB before flushing, because that is faster for bulk work. Your one error line sits in that buffer waiting for company. Measured with a single matching line and a two-second wait:
grep ERROR lines after 2s: 0
grep --line-buffered ERROR lines after 2s: 1
stdbuf -oL grep ERROR lines after 2s: 1
Two fixes, and both work. Ask grep to flush per line, or wrap any command in stdbuf to change its buffering from outside:
$ tail -f app.log | grep --line-buffered ERROR
$ tail -f app.log | grep --line-buffered ERROR | tee errors.txt
$ tail -f app.log | stdbuf -oL cut -d' ' -f1-3
$ tail -f app.log | awk '/ERROR/ { print; fflush() }'
The rule generalises well beyond grep. Any command in the middle of a tail -f pipeline will buffer unless you tell it not to, because none of them can see a terminal from where they are standing. awk needs fflush(), sed needs -u, and anything else needs stdbuf -oL.
5.5 Stopping Automatically
--pid tells tail -f to exit once a given process has finished, which turns "watch this log while that job runs" into a single command:
$ ./long-import.sh &
$ tail -f import.log --pid=$!
line 1
line 2
line 3
$ echo $?
0 # tail noticed the writer had gone
Without it you are left with a tail that never returns, which is fine at a prompt and a problem in a script. Section 6.5 covers the rest of the scripting story.
6. Advanced Use Cases
6.1 Log Rotation, and the Letter That Matters
This is the most important section in the article. Every log on a production server is rotated, usually nightly, and the default tail -f does not survive it. It does not warn you. It just stops showing you anything, forever, while looking exactly like it is working.
Here is the experiment. Two tails on the same file, one with -f and one with -F, across a rename-and-recreate rotation:
$ echo "line 1 before rotation" > app.log
$ tail -f app.log > out-f.txt &
$ tail -F app.log > out-F.txt &
$ echo "line 2 before rotation" >> app.log
$ mv app.log app.log.1 # what logrotate does
$ echo "line 3 AFTER rotation" > app.log
$ echo "line 4 AFTER rotation" >> app.log
And the two results, side by side:
----- tail -f saw -----
line 1 before rotation
line 2 before rotation
# and then nothing. Ever.
----- tail -F saw -----
line 1 before rotation
line 2 before rotation
tail: 'app.log' has become inaccessible: No such file or directory
tail: 'app.log' has appeared; following new file
line 3 AFTER rotation
line 4 AFTER rotation
The reason is a piece of Unix that is worth understanding once, because it explains a dozen other things too. A filename is not a file. Three separate layers are involved:
| Layer | What it is | What a rename does to it |
|---|---|---|
| Pathname | An entry in a directory: /var/log/app.log |
Changes. That is all a rename is. |
| Inode | The actual file: its data and metadata, with no name of its own | Untouched. Same inode, same bytes. |
| File descriptor | A number in one process, pointing at an open inode | Untouched, and still perfectly valid. |
So when logrotate renames app.log to app.log.1, nothing happens to the file tail has open. The directory entry moved; the inode did not. tail -f is still faithfully following the same bytes it always was, which are now called app.log.1 and which nothing will ever write to again.
That is the whole bug, and it is a fair design rather than a mistake. -f follows the file descriptor, which is correct for "watch this exact file" and exactly wrong for "watch this log".
-F follows the name. When the name stops pointing at the file it had open, it says so and reopens. It is shorthand for two longer options:
$ tail -F app.log
$ tail --follow=name --retry app.log # identical
Make -F your default for anything under /var/log. There is no cost: on a file that never rotates, -F behaves exactly like -f.
6.2 The Other Kind of Rotation
The rule above needs one honest qualification, because logrotate has a second mode. With copytruncate, the file is copied and then emptied in place rather than renamed, so the descriptor stays valid. Plain -f handles that case:
$ cp t.log t.log.1 && : > t.log # copytruncate, in two commands
$ echo "after truncate" >> t.log
# what tail -f printed:
before 1
before 2
tail: t.log: file truncated
after truncate # it kept up
So which rotation style your server uses decides whether plain -f quietly fails. You can look:
$ grep -r copytruncate /etc/logrotate.conf /etc/logrotate.d/ | head
The practical advice does not change. Use -F and you do not have to know or care, because it copes with both.
6.3 Waiting for a File That Does Not Exist Yet
-F includes --retry, which means you can start watching before there is anything to watch. This is exactly what you want when you are about to start a service and its log does not exist until it does:
$ tail -F /var/log/myapp/app.log
tail: cannot open '/var/log/myapp/app.log' for reading: No such file or directory
tail: '/var/log/myapp/app.log' has appeared; following new file
[2026-08-24 09:15:02] starting up
Plain tail -f on a missing file prints an error and exits immediately. -F waits. Start the tail first, then start the service, and you will not miss the first lines.
6.4 inotify, and What -s Is Still For
Modern tail -f does not poll. On Linux it asks the kernel to tell it when the file changes, using the same inotify interface that other file-watching tools use. You can confirm it on a running tail:
$ tail -f w.log &
$ ls -l /proc/$!/fd | grep -c inotify
1 # one inotify descriptor
That is why new lines appear instantly rather than up to a second later. The -s option, which sets a sleep interval between checks, is a leftover from the polling era and does almost nothing on a local file today. It still matters in one case: inotify does not work reliably on network filesystems, so on an NFS-mounted log tail falls back to polling and -s becomes real again.
$ tail -f -s 5 /mnt/nfs/shared/app.log # check every 5s on a network mount
6.5 In Scripts
head and tail are well behaved in scripts as long as you avoid -f, which by design never returns. Three patterns cover almost everything:
# bounded: read a fixed amount and move on
last_line=$(tail -n 1 "$logfile")
# follow, but with an owner: tail dies when the job does
./deploy.sh & tail -F deploy.log --pid=$!
# follow, but with a deadline
timeout 300 tail -F deploy.log
Watch out for one interaction. If you write tail -f log | grep -q PATTERN expecting it to exit on the first match, remember that grep -q does exit, but tail may not notice until it next tries to write, so the pipeline can hang. Give it a deadline with timeout, or use the --pid form above.
There is a second interaction, and this one has broken more scripts than anything else in this article. Section 7.2 shows that head kills its producer with SIGPIPE, giving it exit code 141. Normally the shell hides that, because a pipeline reports only the status of its last command. Turn on pipefail and it stops hiding it:
$ bash -c 'yes | head -n 2 >/dev/null; echo $?'
0 # the usual, comfortable answer
$ bash -c 'set -o pipefail; yes | head -n 2 >/dev/null; echo $?'
141 # SIGPIPE, now visible
Now consider how nearly every careful script begins:
#!/bin/bash
set -euo pipefail # the standard safety boilerplate
count=$(seq 1 2000000 | head -n 1) # perfectly ordinary line
echo "reached this line, c=$count" # never printed
$ ./script.sh; echo "script exit: $?"
script exit: 141
set -e saw a non-zero status and stopped. The pipeline did exactly what it was supposed to do, and the script died without a message. This catches people who have done everything right. Three ways out, measured:
seq 1 2000000 | head -n 1 -> 141 the problem
seq 1 2000000 | head -n 1 || true -> 0 tolerate it where it is harmless
seq 1 2000000 | sed -n '1p' -> 0 sed keeps reading, so no SIGPIPE
seq 1 2000000 | sed -n '1{p;q}' -> 141 ... but quit early and it is back
That third line has a cost worth naming: sed -n '1p' avoids the signal precisely because it reads all two million lines instead of stopping. It trades a status code for the work head was saving you. On a big input, || true is the better answer.
The same applies to grep -q and to anything else that closes a pipe before its producer has finished.
7. Something Most Users Do Not Know
7.1 tail Does Not Read Your File
People avoid tail on huge files, imagining it grinding through gigabytes to reach the end. It does not. When the input is a real file it seeks straight to the end and reads backwards from there. head does the mirror image: it reads from the start and stops as soon as it has enough.
Measured on a 1.1 GB text file:
$ tail -n 5 huge.txt 0.00 s # seeks to the end
$ head -n 5 huge.txt 0.00 s # stops after 5 lines
$ cat huge.txt | tail -n 5 0.32 s # the pipe forces all 1.1 GB through
$ sed -n '5p' huge.txt 0.43 s # parses every line to find one
Two lessons in one table. First, tail on a huge log is free, so stop hesitating. Second, the classic useless use of cat is not merely inelegant here: cat huge.txt | tail -n 5 genuinely reads a gigabyte that tail huge.txt never touches, because a pipe cannot be seeked. The file argument is not a style preference.
You can watch it happen. strace on a 1,288,895-byte file shows the whole algorithm in four system calls:
$ strace -e trace=lseek,read tail -n 3 s.txt
lseek(3, 0, SEEK_CUR) = 0
lseek(3, 0, SEEK_END) = 1288895 # how big is it?
lseek(3, 1286144, SEEK_SET) = 1286144 # jump to one block from the end
read(3, "199608\n199609\n199610\n"..., 2751) = 2751
read(3, "", 0) = 0 # done
Size, seek, read one block, count newlines backwards, print. It read 2751 bytes out of 1.3 megabytes. If that block had not held three newlines it would have stepped back another block and tried again, and that is the entire implementation.
Now the same command with the file arriving through a pipe:
$ cat s.txt | strace -e trace=lseek tail -n 3
+++ exited with 0 +++ # not a single lseek
No seeks at all, because there is nothing to seek. A pipe has no size and no past: bytes arrive once and are gone. So tail switches strategy entirely and keeps a rolling buffer of the most recent lines as the stream flows past, discarding each one as a newer arrives. It works, it is bounded, and it costs a read of every single byte.
That is the real reason the two commands in the table above differ by a factor of infinity rather than a few percent. It is not that cat is wasteful in some abstract way. It is that handing tail a pipe takes away the only trick it has.
7.2 head Kills the Thing Feeding It
What happens to the command on the left when head has seen enough?
$ yes | head -n 3
y
y
y
$ bash -c 'yes | head -n 2 >/dev/null; echo "producer=${PIPESTATUS[0]}"'
producer=141
141 is 128 plus 13, and signal 13 is SIGPIPE. When head exits it closes the read end of the pipe, the next write from yes has nowhere to go, and the kernel kills it. That is the mechanism that makes yes | head terminate instead of filling your disk, and it is why somecommand | head -n 5 is safe on a command that would otherwise run forever.
It also explains an error message people find alarming. A script that pipes into head may report an exit status of 141, or print "Broken pipe" if the producer chose to handle the signal loudly. That is not a failure, it is the pipeline working as designed.
7.3 The Obsolete Syntax That Still Works, Until It Does Not
tail -5 is the old pre-POSIX form. It still works, and it is fine at a prompt. The trap is that the modern option letters occupy the same space:
$ tail -5 file # 5 lines. The obsolete form.
$ tail -n 5 file # 5 lines. The POSIX form.
$ tail -c 5 file # 5 BYTES, not lines.
$ tail -f -5 file # works, but now you are mixing two eras
Any script that survives long enough will eventually be read by someone who has to work out whether -5 meant lines or bytes. Write -n 5 and the question never arises. The same applies to head.
7.4 The Negative and Plus Forms Nobody Uses
Section 1.3 introduced them; they deserve a second look because each solves a problem people usually solve the hard way.
$ head -n -1 file.csv # everything except the LAST line
$ head -c -4 file.bin # everything except the last 4 bytes
$ tail -c +2 file.txt # from byte 2 onwards: strips one leading byte
$ tail -n +2 file.csv # from line 2 onwards: strips the header
head -n -1 is the one worth remembering. Dropping a trailing summary line, a total row, or a stray blank line at the end of a generated file is otherwise a job for sed or awk and a moment of thought. And tail -c +2 has a genuine niche: removing a leading byte-order mark or a single stray character from the front of a file without touching anything else.
7.5 A Missing Final Newline Stays Missing
Text files are supposed to end with a newline, and plenty do not. tail does not quietly fix that for you:
$ printf 'x1\nx2' > nn.txt # no newline after x2
$ tail -n 1 nn.txt | xxd
00000000: 7832 x2 # two bytes. No 0a.
That is correct and occasionally surprising. If you build a file by appending tail -n 1 output in a loop, the lines will run together the moment one source file lacks its final newline. It is also why your shell prompt sometimes appears glued to the end of command output.
7.6 tail in Containers, Including the One Everybody Copies
Containers change the answer twice, and both are worth knowing before you shell into one out of habit.
First, a well-built container image does not write log files. It writes to standard output and standard error, and the runtime collects them. So docker exec followed by tail -f /var/log/something usually finds nothing, because nothing is there:
$ docker logs -f --tail 100 mycontainer # the actual equivalent
$ docker compose logs -f web
$ kubectl logs -f deploy/web --tail=100
Those are the tail -F of the container world, and they take the same two arguments you already know: how much history, and whether to follow. Reach for tail inside a container only when the application genuinely does write files, which usually means it was containerised without being adapted.
Second, there is a line you will meet in a hundred Dockerfiles and Compose files:
command: tail -f /dev/null
This has nothing to do with logs. A container lives exactly as long as its main process, and /dev/null never produces any data, so tail -f on it blocks forever without using any CPU. It is a cheap way to say "start this container and keep it running so I can exec into it".
As a debugging trick that is fine. As something in a committed file it is a smell, because it means the container has no real main process: nothing to supervise, nothing whose exit signals failure, and a restart policy that can never trigger because tail never dies. Health checks pass while the actual application is not running at all. If you find it in a production compose file, the question to ask is what that container is supposed to be doing.
sleep infinity does the same job with one fewer surprise, and no reader will mistake it for something to do with logging.
7.7 Knowing Where head and tail Stop
tail -f is the right tool more often than not, but it has clear edges:
| When you need | Reach for |
|---|---|
| To scroll back while still following | less +F: Ctrl-C to browse, F to resume |
| The systemd journal rather than a file | journalctl -f, which also has -n and -u |
| Many logs in one screen, with colour | multitail, or lnav for structured logs |
| Logs from several machines | Central logging; tail -F over SSH does not scale |
| To run something when a file changes | entr or inotifywait, not a tail |
| A range that depends on content, not position | sed -n '/start/,/end/p' or awk |
| To watch a value change, not a log grow | watch, which re-runs a command instead |
less +F deserves the top row. It does everything tail -f does, and then Ctrl-C drops you into a full pager over the same file, so you can search backwards through what already scrolled past and press F to start following again. For any session where you expect to read as well as watch, it is simply better.
8. Best Practices
- Use
-F, not-f, on anything in/var/log. Rename-based rotation makes plain-fgo silently deaf, and-Fcosts nothing on a file that never rotates. - Write
-n 20, not-20. The bare number is the obsolete pre-POSIX form. It still works and it will still confuse the next reader about lines versus bytes. - Learn
tail -n +2. "Start at line 2" is the clean way to drop a header row, and it beatsgrep -vagainst the header text, which also eats matching data. - Remember
head -n -1. "Everything except the last line" saves reaching forsedwhenever a file has a trailing total or summary. - Pass the filename, do not
catinto it.tail -n 5 fileseeks to the end;cat file | tail -n 5reads the whole thing. On a gigabyte log that is 0.00s against 0.32s. - Add
--line-bufferedtogrepin anytail -fpipeline, and-utosed,fflush()toawk, orstdbuf -oLto anything else. Otherwise your matches sit in a 4 KB buffer. - Start with more context than the default.
tail -n 100 -Fshows you what led up to the thing you are waiting for; ten lines rarely does. - Use
--pid=$!when following a job's log. The tail then ends when the job does, instead of hanging around forever. - Never put a bare
tail -fin a script. It does not return by design. Bound it with--pidortimeout. - Use
-qwhen feeding multiple files onward. Those==>header lines are data to the next command in the pipeline. - Reach for
less +Fwhen you will want to read as well as watch. Ctrl-C to scroll back and search,Fto resume following. - Prefer
tail -n +8 | head -n 5overhead -n 12 | tail -n 5for a middle range. Same answer, and it stops reading once it has what it needs. - Guard
| headin any script withset -o pipefail. Write| head -n 1 || truewhere the SIGPIPE is harmless, or the script dies at exit 141 having done nothing wrong. - Count lines, not bytes, on text with accents.
-ccan cut a UTF-8 character in half and produce invalid output. - Use the runtime's own command inside containers.
docker logs -f --tail 100andkubectl logs -fare the real equivalents, because a good image logs to stdout and has no file to tail. - Read the manual for the two you will forget.
--retryand--follow=nameare what-Fis made of, and knowing that makes its behaviour obvious rather than magical.
$ man 1 tail # short, and the -f/-F explanation is worth reading
$ man 1 head
$ info coreutils 'tail invocation' # deeper than the man page
$ man 1 less # for +F
$ man 5 logrotate.conf # to see which rotation style your server uses
Back to top9. Common Mistakes
9.1 Myth Versus Reality
| Myth | Reality |
|---|---|
"tail -f keeps following my log." |
Until it rotates. Then it follows the renamed old file forever, silently. Use -F (section 6.1). |
"tail has to read the whole file to reach the end." |
It seeks. 0.00s on a 1.1 GB file. It is cat file | tail that reads everything. |
"tail -f | grep is broken, nothing appears." |
grep buffers about 4 KB when not writing to a terminal. Add --line-buffered (section 5.4). |
"tail -n +5 shows the last five lines." |
It shows everything from line 5 to the end. Without the plus it is a count; with it, a starting point. |
"-5 and -n 5 are the same thing." |
They give the same result, but -5 is the obsolete form, and -c 5 next to it means bytes. |
"head and tail were written as a pair." |
head came from 1BSD in 1978, tail from Version 7 Unix in 1979. Different origins, a year apart. |
"Piping into head causes a Broken pipe error." |
It causes SIGPIPE, exit 141, and that is the design: it is how the producer gets told to stop. |
"tail -f polls the file once a second." |
Not on Linux. It uses inotify and reacts immediately. -s only matters on network filesystems. |
"tail -f on a file that does not exist yet just fails." |
Plain -f does. -F includes --retry and waits for the file to appear. |
"Plain -f always breaks on rotation." |
Not with copytruncate, where the descriptor stays valid. It prints "file truncated" and continues (section 6.2). |
"tail -n 1 always outputs a complete line." |
If the file has no final newline, neither does the output. It copies what is there. |
"Piping into head is always safe in a script." |
Under set -o pipefail the pipeline reports 141, and set -e then aborts the script. Extremely common boilerplate, entirely silent failure (section 6.5). |
"-F is a GNU extension I cannot rely on." |
BusyBox has it too. What BusyBox does not have is --pid, --retry or any long option at all (section 3). |
"tail -c 100 gives me the last 100 characters." |
Bytes, not characters. On UTF-8 text it can start mid-character and produce output that is not valid UTF-8 (section 4.4). |
"tail -f /dev/null in a Dockerfile is a logging thing." |
It is a "keep this container alive" hack. It also means the container has no real main process to supervise (section 7.6). |
"I can put tail -f in a script and it will finish." |
It never returns. Bound it with --pid or timeout. |
9.2 Other Traps to Avoid
- Following a symlink with
-f. Repoint the link at a new file and-fstays on the old target forever.-Fdoes notice, but by periodic checking rather than instantly, so expect a few seconds of silence before it saystail: 'link.log' has been replaced; following new file. Worth knowing before you conclude it is not working. - Forgetting
-qwhen piping several files. The==>headers become input to whatever comes next, and they will quietly corrupt a count or a sort. - Using
headto sample a binary file without-c. Line-based tools on binary data can emit terminal escape sequences and scramble your prompt. Use-cand pipe throughxxd. - Assuming the last line is complete. Read the final line of a log that is being written right now and you may catch it half-written. This is a genuine source of parsing bugs in monitoring scripts.
- Watching a log you do not have permission to read.
tail -Fwill sit there retrying rather than telling you clearly. Check withls -lfirst if nothing appears. - Leaving
tail -fon a log that has been deleted. Holding the descriptor keeps the inode alive, so the space is not freed. Measured with a 200 MB file deleted under a running tail: free space did not move onrm, and all 200 MB came back the instanttailexited. This is the classic reasondfanddudisagree, and why a full disk sometimes empties itself when you close a forgotten terminal. - Forgetting that log files hold secrets. Session tokens, API keys, password-reset links and personal data all end up in access logs. A
tail -fon a shared screen, in a recorded call, or pasted into a ticket leaks whatever scrolled past. - Letting a slow consumer throttle the producer. A pipe has a fixed buffer. If the command after
tail -fcannot keep up, the pipe fills and writes block, so a heavy downstream filter can slow down the thing you are watching. Keep the filter cheap and do the expensive work later. - Using
tailto get a "random" sample. The end of a log is the most recent data, not a representative one. For sampling, useshuf -n. - Chaining
tail | headthe slow way round.head -n 1000000 | tail -n 10reads a million lines;tail -n +999991 | head -n 10does not. - Expecting
-fto show you what you missed. It starts from the current end plus the last-nlines. Anything written before you started, beyond that, is not shown. - Running
tail -Fover SSH as a monitoring strategy. It works for one server and one afternoon. It is not logging infrastructure.
10. Summary
head and tail are among the first commands anybody learns and among the last anybody reads the manual for. The parts worth knowing are the plus sign, the letter F, and the buffer in the middle of your pipeline.
- Both default to ten lines.
-nchanges the count,-cswitches to bytes, and both read standard input when given no file. - The sign changes the question.
tail -n 3is "the last three";tail -n +3is "from line 3 to the end".head -n -3is "everything except the last three". tail -n +2drops a header row.head -n -1drops a trailing summary. Those two cover most of what people writesedfor.tail -fdies silently when a log rotates. It follows the file descriptor, so it ends up watching the renamed old file.-Ffollows the name, says what happened, and reopens.-Fis exactly--follow=name --retry, which also means it will wait for a file that does not exist yet.- The exception: with
copytruncaterotation the descriptor stays valid, so plain-fcopes and prints "file truncated". Use-Fand you never need to know which style your server uses. tail -f | grepappears broken becausegrepbuffers. Add--line-buffered, orstdbuf -oLfor anything else in the pipeline.tailseeks: 0.00s to read the end of a 1.1 GB file.cat file | tailreads all of it, because a pipe cannot be seeked. Pass the filename.headclosing a pipe sends SIGPIPE to the producer, which exits 141. That is what makesyes | headstop rather than run forever.- On Linux
tail -fuses inotify and reacts instantly.-sis a polling-era option that only matters on network filesystems. --pid=$!makes a follow end when the job it is watching ends. Never put a baretail -fin a script.- Multiple files get
==>headers, including while following.-qremoves them,-vforces them. - A file with no final newline produces output with no final newline.
tailcopies what is there. headis from 1BSD in 1978 andtailfrom Version 7 Unix in 1979. Write-n 5, not-5: the bare number predates POSIX.- The seek is visible in four system calls:
SEEK_ENDfor the size,SEEK_SETto a block from the end, one read of 2751 bytes out of 1.3 MB. Through a pipe there are no seeks at all, sotailfalls back to a rolling buffer and reads every byte. - A filename is not a file. Renaming moves a directory entry; the inode and any open descriptor are untouched. That is exactly why
tail -fkeeps reading a log nothing writes to any more. - Under
set -o pipefail,| headmakes a pipeline report 141, andset -ethen aborts the script. Use|| truewhere the SIGPIPE is harmless. -Fworks on BusyBox too.--pid,--retryand the long options are GNU-only, which matters inside minimal container images.-ccounts bytes, so it can split a UTF-8 character and produce invalid output. Count lines on text with accents.- In containers, use
docker logs -forkubectl logs -f. Andtail -f /dev/nullis not logging: it is a keep-alive hack that hides the fact the container has no real main process. - For reading as well as watching,
less +Fbeatstail -f. For the journal,journalctl -f.
This is the quick reference worth keeping:
THE BASICS (both default to 10 lines)
head -n 20 FILE first 20 lines
tail -n 20 FILE last 20 lines
head -c 512 FILE first 512 bytes (K=1024, kB=1000)
tail -c 100 FILE last 100 bytes
THE SIGN CHANGES THE QUESTION
tail -n +2 FILE from line 2 on <- drops a header row
head -n -1 FILE all but the last line <- drops a summary row
tail -c +2 FILE from byte 2 on <- strips a leading byte
head -c -4 FILE all but the last 4 bytes
A RANGE IN THE MIDDLE (lines 8-12)
tail -n +8 FILE | head -n 5 preferred: stops reading early
head -n 12 FILE | tail -n 5 same answer, reads more
FOLLOWING A LOG
tail -F FILE USE THIS. = --follow=name --retry
tail -f FILE follows the descriptor: DIES on rotation
tail -n 100 -F FILE start with real context
tail -F a.log b.log several at once, with ==> headers
tail -F FILE --pid=$! exit when the writer exits
timeout 300 tail -F FILE exit after 5 minutes
less +F FILE follow, Ctrl-C to scroll back, F to resume
UNBUFFERING A FOLLOW PIPELINE (or nothing appears)
tail -F log | grep --line-buffered ERROR
tail -F log | sed -u 's/x/y/'
tail -F log | awk '/E/ { print; fflush() }'
tail -F log | stdbuf -oL ANY-OTHER-COMMAND
MULTIPLE FILES
tail -q -n 1 *.log no ==> headers (use when piping onward)
tail -v -n 1 one.log force a header on a single file
IN CONTAINERS (a good image logs to stdout, so there is no file)
docker logs -f --tail 100 NAME
docker compose logs -f SERVICE
kubectl logs -f deploy/NAME --tail=100
tail -f /dev/null NOT logging: a keep-alive hack. Prefer sleep infinity.
PORTABILITY
-n -c -q -v -f -F -s -n +N -n -N GNU and BusyBox both
--pid --retry --follow=name GNU only (absent in BusyBox)
SCRIPTS
set -o pipefail + | head -> exit 141, and set -e aborts. Use || true.
tail -c N on UTF-8 text can split a character. Count lines instead.
tail SEEKS to the end: pass the filename, never cat into it
exit 141 from a producer = SIGPIPE = head closed the pipe. Normal.
-5 is the obsolete pre-POSIX form. Write -n 5.
If you take one habit from this article, make it the capital F. Most people discover the difference the hard way, at two in the morning, staring at a log that stopped saying anything at midnight and wondering why the problem went quiet. And if a server keeps producing symptoms that nobody can pin down, the answer is usually already written in a log that nobody was following properly.


Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.












