tail Command in Linux: Complete Guide to Viewing End of Files, Following Logs, and Parameters

tail command in Linux and it perimeters

tail command in Linux and it perimeters

The first time tail -f genuinely saved me was during an on-call shift when a payment service started throwing 500s and I needed to watch the application log update in real time while a deploy rolled out. I didn’t have time to keep re-opening the log in a text editor. tail -f just sat there in my terminal and streamed every new line as it was written. That’s the moment tail went from “a command I knew existed” to “a command I use every single day.”

tail prints the last part of a file — by default the last 10 lines — and it can also follow a file as it grows, which makes it one of the most important tools for anyone doing Linux system administration, DevOps, or debugging.

What tail Does

At its core, tail reads a file (or standard input) and outputs the tail end of it. The opposite of head, which shows the beginning.

tail [OPTIONS] [FILE]...

I tested the default behavior against a 20-line file:

$ seq 1 20 > nums.txt
$ tail nums.txt
11
12
13
14
15
16
17
18
19
20

Ten lines, no options needed — that’s the default.

Basic Syntax and Core Options

-n NUMBER — Show a Specific Number of Lines

$ tail -n 5 nums.txt
16
17
18
19
20

You can also write this as tail -5 nums.txt in most implementations, though -n 5 is more explicit and portable.

-n +NUMBER — Show From a Specific Line to the End

This one is less well known but incredibly useful — instead of counting from the end, it counts from the start:

$ tail -n +15 nums.txt
15
16
17
18
19
20

This prints everything starting at line 15. I use this a lot when skipping a header block in a generated file, e.g., tail -n +2 data.csv to strip a CSV header before piping into another tool.

-c NUMBER — Show Last N Bytes

$ tail -c 10 nums.txt

18
19
20

Instead of counting lines, this counts raw bytes from the end of the file. Useful for binary-adjacent files or when you need an exact byte offset rather than a line count.

-f — Follow the File in Real Time

This is the option most people associate with tail. It keeps the file open and prints new lines as they’re appended, which is exactly what you want when watching a live log:

tail -f /var/log/syslog

The process blocks and keeps running until you hit Ctrl+C. Internally, tail -f polls the file’s size at intervals (or uses inotify on Linux, depending on implementation) and prints any new bytes appended since the last check.

-F — Follow, and Retry on Rotation

This is the option I actually reach for on production servers instead of plain -f. Log rotation (via logrotate) renames or truncates the current log file and creates a new one with the same name. Plain -f keeps watching the original inode, so once rotation happens, it stops receiving updates. -F (equivalent to --follow=name --retry) re-opens the file by name if it detects the file has been replaced:

tail -F /var/log/nginx/access.log

This one flag has saved me from the classic “why did my log monitoring silently stop working at 3 AM when logrotate fired” problem.

--pid=PID — Stop Following When a Process Dies

Combine -f with --pid to have tail automatically stop once a given process exits:

tail -f --pid=1234 app.log

This is handy in scripts that start a background service, tail its log until the process finishes, and then move on.

-q — Quiet Mode (Suppress Headers) for Multiple Files

When you tail more than one file, GNU tail prints a header before each file’s output by default:

$ tail -n 2 f1.txt f2.txt
==> f1.txt <==
b
c

==> f2.txt <==
2
3

Use -q to suppress those headers:

tail -q -n 2 f1.txt f2.txt

Or force headers even for a single file with -v (verbose).

-s SECONDS — Sleep Interval Between Polls (with -f)

Controls how frequently tail -f checks for new data (default is 1 second on most systems):

tail -f -s 5 /var/log/app.log

Lower values give faster updates at the cost of slightly more CPU; higher values reduce overhead for logs that update infrequently.

How tail Works Internally

For a plain (non-follow) tail, GNU coreutils’ implementation is smart about seeking: if the input is a regular file, tail can seek near the end of the file directly using lseek() rather than reading the whole file from the beginning, which is what makes tail -n 1000 huge_file.log fast even on multi-gigabyte files. If it can’t determine the file size (e.g., reading from a pipe), it falls back to reading everything and buffering the last N lines in memory.

For -f, the implementation depends on the underlying platform. On Linux, GNU tail typically uses inotify to get notified the moment new data is written to the file, rather than polling in a tight loop — this is far more efficient on modern systems, though it transparently falls back to polling on filesystems that don’t support inotify (like some network filesystems).

Practical, Real-World Examples

1. Watching a Web Server Log Live

tail -f /var/log/nginx/access.log

2. Watching Multiple Logs at Once

tail -f /var/log/nginx/access.log /var/log/nginx/error.log

Each new line is prefixed by which file it came from, which is great for correlating access and error events during an incident.

3. Following a systemd Service Log Alongside journalctl

While journalctl -f is the modern equivalent for services logging via systemd’s journal, tail -F is still essential for any application still writing to flat log files, which is most legacy and many containerized apps.

4. Grabbing the Last Error from a Massive Log File

tail -n 100 /var/log/app.log | grep -i error

I use this pattern constantly when I don’t want to grep the entire multi-gigabyte log file, just the recent history.

5. Stripping a CSV Header Before Processing

tail -n +2 data.csv | sort -t, -k3 -n

6. Watching a Log Until a Deploy Script Finishes

./deploy.sh &
tail -f --pid=$! deploy.log

This tails the deploy log and automatically stops once the deploy script’s background process exits — no manual Ctrl+C needed.

tail in Shell Scripting and Automation

A pattern I use in health-check scripts is checking whether the last few lines of a log contain an error marker:

#!/bin/bash
LOG="/var/log/myapp/app.log"
if tail -n 50 "$LOG" | grep -q "FATAL"; then
  echo "Fatal error detected in recent logs" | mail -s "ALERT: myapp" ops@example.com
fi

Run this via cron every few minutes and you have a crude but genuinely effective log-based alerting system without needing a full monitoring stack.

Another pattern: rotating custom application logs manually before a logrotate config is in place, by capturing the tail of the file before truncating:

tail -n 10000 app.log > app.log.recent
> app.log   # truncate the original

Comparing tail to Related Commands

TaskBest Tool
Beginning of filehead
End of filetail
Live-following a growing filetail -f / tail -F
Live-following systemd journaljournalctl -f
Reversing entire file ordertac
Paging through a file interactivelyless

tail -f and less +F are functionally similar — less +F gives you a “follow mode” you can interrupt with Ctrl+C and then scroll around in, then resume follow mode with F. If I need to both watch live output and occasionally scroll back, I’ll use less +F instead of plain tail -f.

Troubleshooting Common tail Issues

tail -f stops updating after log rotation — switch to tail -F, which reopens the file by name.

High CPU usage with tail -f on network filesystems — inotify doesn’t work reliably on some NFS/CIFS mounts, so tail falls back to polling, which can be CPU-intensive on very frequent polling intervals. Increase the poll interval with -s or, better, avoid tailing logs directly over NFS.

Permission denied — you need read access to the file; on many systems, application or system logs under /var/log require sudo or membership in a specific group (like adm on Debian-based systems).

Only whitespace shows up when tailing a binary file with -c — this is expected since raw bytes from a binary file often aren’t printable; pipe through xxd or od -c instead if you need a readable byte dump.

Performance Optimization

For plain tail -n, performance is rarely a concern thanks to the seek-based implementation described earlier. Where performance actually matters is with tail -f on very high-throughput logs — if a service writes thousands of lines per second, piping tail -f into a slow consumer (like a script doing heavy per-line processing) can create backpressure. In those cases, consider a proper log shipper (Filebeat, Fluentd, Vector) instead of a shell pipeline for production-grade log streaming.

Security Implications

Tailing log files can inadvertently expose sensitive data — session tokens, stack traces with internal paths, or PII — to anyone with terminal access or watching over your shoulder during a screen share. Be deliberate about who has read access to logs (chmod/chown and group membership), and consider redacting sensitive fields at the application logging layer rather than relying on restricting tail access alone.

Compatibility Across Distributions

tail (GNU coreutils version, tested here at 9.4) is standard on Ubuntu, Debian, Fedora, RHEL/CentOS, Arch, and openSUSE. BusyBox’s tail (common on Alpine Linux and embedded systems) supports the core options (-n, -f, -c) but lacks some GNU extensions like --pid and -F‘s retry-on-rename behavior — check tail --help on the target system before relying on advanced flags in portable scripts.

tail and inotify: A Closer Look at Real-Time Following

It’s worth spending a bit more time on exactly how tail -f achieves its real-time behavior, since it’s genuinely one of the more elegant pieces of engineering hiding behind a very simple-looking command. On Linux, the inotify subsystem lets a process register interest in filesystem events for a specific file or directory — creation, modification, deletion, renaming — and receive those notifications from the kernel asynchronously, without needing to repeatedly ask “has anything changed?” in a busy-wait loop. GNU tail, when following a file with -f, sets up an inotify watch on the target file and blocks efficiently until the kernel signals that new data has been written, at which point it reads and prints exactly the new bytes appended since the last read.

This event-driven approach is dramatically more efficient than naive polling, especially across many simultaneously tailed files or on systems where CPU efficiency genuinely matters (embedded systems, high-density container hosts). It’s also why tail -f on most local Linux filesystems responds to new log lines essentially instantaneously, rather than with the noticeable lag you’d get from, say, a polling interval of a full second. On filesystems that don’t support inotify — certain network filesystems, some virtual/overlay filesystems used in containers — GNU tail detects this and transparently falls back to a polling strategy instead, which is both slower to notice changes and more CPU-intensive under heavy load, since it’s now actively checking the file’s state at a fixed interval rather than waiting to be notified.

tail in Container and Kubernetes Environments

tail -f (and tail -F) come up constantly in containerized environments, and it’s worth understanding a specific quirk that trips people up. Inside a container, tail -f on a log file works exactly as described above. But a very common pattern for making a container’s main process log-friendly for orchestrators like Kubernetes is:

tail -f /var/log/app/current.log

…run as the container’s foreground (PID 1) process, so that docker logs or kubectl logs can capture whatever tail outputs. This works because tail -f, run in the foreground, never exits on its own — it blocks indefinitely waiting for new content, which is exactly the behavior needed to keep a container alive and continuously streaming logs to the orchestrator’s logging pipeline. Understanding this pattern demystifies a surprising number of container images you’ll encounter that otherwise look like they’re “just tailing a log file” for no obvious reason — that tail process often is the entire reason the container stays running.

Handling Log Rotation Explicitly With logrotate and tail

Since -F was already covered as the practical fix for rotation-aware following, it’s worth understanding what logrotate actually does that breaks plain -f in the first place. A typical logrotate cycle either renames the current log file (e.g., app.log becomes app.log.1) and creates a fresh, empty app.log, or truncates the existing file to zero length in place (using the copytruncate directive). In the rename case, plain tail -f keeps watching the original inode — which is now app.log.1 — and never notices that a new app.log has appeared, since it was watching a specific open file descriptor, not a filename. -F solves this specifically by periodically checking whether the filename it was given now refers to a different underlying file than the one it’s currently watching, and if so, reopening it. In the truncate case (copytruncate), even plain -f handles it reasonably well since the inode doesn’t change, but -F‘s behavior remains correct in both cases, which is why it’s the safer default choice for any log expected to rotate.

Summary

tail is deceptively simple on the surface — “show me the end of the file” — but between -f/-F for real-time log following, -n +NUMBER for skipping headers, --pid for automation, and its efficient seek-based implementation on large files, it’s one of the most load-bearing commands in day-to-day Linux administration. If you only remember two things from this guide, remember -F for log rotation resilience and -n +N for skipping past a header line.

References

Exit mobile version