Skip to main content

Linux command: tail

02 September 2026

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:

OptionShort forMeaning
-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.

FormMeansOn 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.

The 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 head or tail one-liner is really about that distinction.

Back to top

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.

Back to top

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.

EraMilestone
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:

FeatureGNU coreutilsBusyBox 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.

Back to top

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 top

5. 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.

Back to top

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:

LayerWhat it isWhat 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.

Back to top

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 needReach 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.

Back to top

8. Best Practices

  • Use -F, not -f, on anything in /var/log. Rename-based rotation makes plain -f go silently deaf, and -F costs 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 beats grep -v against the header text, which also eats matching data.
  • Remember head -n -1. "Everything except the last line" saves reaching for sed whenever a file has a trailing total or summary.
  • Pass the filename, do not cat into it. tail -n 5 file seeks to the end; cat file | tail -n 5 reads the whole thing. On a gigabyte log that is 0.00s against 0.32s.
  • Add --line-buffered to grep in any tail -f pipeline, and -u to sed, fflush() to awk, or stdbuf -oL to anything else. Otherwise your matches sit in a 4 KB buffer.
  • Start with more context than the default. tail -n 100 -F shows 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 -f in a script. It does not return by design. Bound it with --pid or timeout.
  • Use -q when feeding multiple files onward. Those ==> header lines are data to the next command in the pipeline.
  • Reach for less +F when you will want to read as well as watch. Ctrl-C to scroll back and search, F to resume following.
  • Prefer tail -n +8 | head -n 5 over head -n 12 | tail -n 5 for a middle range. Same answer, and it stops reading once it has what it needs.
  • Guard | head in any script with set -o pipefail. Write | head -n 1 || true where the SIGPIPE is harmless, or the script dies at exit 141 having done nothing wrong.
  • Count lines, not bytes, on text with accents. -c can cut a UTF-8 character in half and produce invalid output.
  • Use the runtime's own command inside containers. docker logs -f --tail 100 and kubectl logs -f are 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. --retry and --follow=name are what -F is 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 top

9. Common Mistakes

9.1 Myth Versus Reality

MythReality
"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 -f stays on the old target forever. -F does notice, but by periodic checking rather than instantly, so expect a few seconds of silence before it says tail: 'link.log' has been replaced; following new file. Worth knowing before you conclude it is not working.
  • Forgetting -q when piping several files. The ==> headers become input to whatever comes next, and they will quietly corrupt a count or a sort.
  • Using head to sample a binary file without -c. Line-based tools on binary data can emit terminal escape sequences and scramble your prompt. Use -c and pipe through xxd.
  • 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 -F will sit there retrying rather than telling you clearly. Check with ls -l first if nothing appears.
  • Leaving tail -f on 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 on rm, and all 200 MB came back the instant tail exited. This is the classic reason df and du disagree, 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 -f on 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 -f cannot 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 tail to get a "random" sample. The end of a log is the most recent data, not a representative one. For sampling, use shuf -n.
  • Chaining tail | head the slow way round. head -n 1000000 | tail -n 10 reads a million lines; tail -n +999991 | head -n 10 does not.
  • Expecting -f to show you what you missed. It starts from the current end plus the last -n lines. Anything written before you started, beyond that, is not shown.
  • Running tail -F over SSH as a monitoring strategy. It works for one server and one afternoon. It is not logging infrastructure.
Back to top

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. -n changes the count, -c switches to bytes, and both read standard input when given no file.
  • The sign changes the question. tail -n 3 is "the last three"; tail -n +3 is "from line 3 to the end". head -n -3 is "everything except the last three".
  • tail -n +2 drops a header row. head -n -1 drops a trailing summary. Those two cover most of what people write sed for.
  • tail -f dies silently when a log rotates. It follows the file descriptor, so it ends up watching the renamed old file. -F follows the name, says what happened, and reopens.
  • -F is exactly --follow=name --retry, which also means it will wait for a file that does not exist yet.
  • The exception: with copytruncate rotation the descriptor stays valid, so plain -f copes and prints "file truncated". Use -F and you never need to know which style your server uses.
  • tail -f | grep appears broken because grep buffers. Add --line-buffered, or stdbuf -oL for anything else in the pipeline.
  • tail seeks: 0.00s to read the end of a 1.1 GB file. cat file | tail reads all of it, because a pipe cannot be seeked. Pass the filename.
  • head closing a pipe sends SIGPIPE to the producer, which exits 141. That is what makes yes | head stop rather than run forever.
  • On Linux tail -f uses inotify and reacts instantly. -s is 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 bare tail -f in a script.
  • Multiple files get ==> headers, including while following. -q removes them, -v forces them.
  • A file with no final newline produces output with no final newline. tail copies what is there.
  • head is from 1BSD in 1978 and tail from Version 7 Unix in 1979. Write -n 5, not -5: the bare number predates POSIX.
  • The seek is visible in four system calls: SEEK_END for the size, SEEK_SET to a block from the end, one read of 2751 bytes out of 1.3 MB. Through a pipe there are no seeks at all, so tail falls 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 -f keeps reading a log nothing writes to any more.
  • Under set -o pipefail, | head makes a pipeline report 141, and set -e then aborts the script. Use || true where the SIGPIPE is harmless.
  • -F works on BusyBox too. --pid, --retry and the long options are GNU-only, which matters inside minimal container images.
  • -c counts bytes, so it can split a UTF-8 character and produce invalid output. Count lines on text with accents.
  • In containers, use docker logs -f or kubectl logs -f. And tail -f /dev/null is 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 +F beats tail -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.

Back to top
Linux command: tail
Peter Martin
Peter Martin
Joomla Specialist

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