On this page

Everything in the terminal chapters works, until the day it does not. A command vanishes, a disk fills, a service refuses to start. This guide is about reading those moments instead of fighting them: six failure messages, each traced to a cause by the shortest command path that separates the candidates.

It stands on its own, but it leans on the chapters for depth. Everything here was verified on Ubuntu 26.04, Fedora 44 and Arch containers, and where the distros disagree, the text says so.

Read the error, then shrink the problem

Every failure starts as text on a screen, and the text is usually honest. The habit that separates a five-minute fix from an evening of guessing is three questions, asked in order:

  1. What did it say, exactly? Not “it broke”. The exact words, copied out.
  2. What changed since it worked? An update, a moved file, a new disk, a full one.
  3. What is the smallest test that still fails? One command instead of a whole workflow, one file instead of a directory.

The third question is the workhorse. “The internet is down” becomes “can I reach this address”, which becomes “does any name resolve”, and each shrink either finds the broken layer or eliminates it. The symptom sections below are that shrinking, written out.

A frozen terminal has its own small arithmetic. Ctrl+C interrupts a running command — a ping that forgot to stop, mostly, which is why the networking chapter teaches ping -c so the command ends by itself. And if the screen went silent right after Ctrl+S, nothing crashed: Ctrl+S pauses terminal output and Ctrl+Q starts it flowing again.

The commands here are deliberately brief, because each has a chapter that teaches it properly — navigating the filesystem and files and directories first among them, then permissions, processes, packages, networking, disks, the shell, and services and logs. Six symptom sections follow, plus a logs section and a full drill at the end.

“command not found”

$ xyzzy
bash: xyzzy: command not found
$ echo $?
127

Bash has never played the adventure games that made xyzzy a magic word, so it just reports the absence. Exit code 127 is the formal statement that nothing by this name exists, and the bash guide’s exit-codes section explains the whole zero-versus-nonzero business. Ubuntu desktop installs often append a package hint to this error. It is a convenience layer, and the message means the same without it.

Three different causes produce this one message, and the shrinking questions sort them out.

It might be a typo

type is the built-in detective. It answers for real commands, shell builtins and nonsense alike:

$ type xyzzy
bash: type: xyzzy: not found
$ type cd
cd is a shell builtin
$ type ls
ls is /usr/bin/ls

which does the narrower job of printing just the path. Trust the fact that a path came back more than the path itself: on our Ubuntu container which ls answered /usr/bin/ls and on Fedora 44 and Arch it answered /usr/sbin/ls. Same program, different parking spot per distro.

It might not be installed

Each distro can tell you who ships a program. Same question, three dialects:

$ apt search traceroute
$ dnf provides '*/traceroute'
$ pacman -Fy
$ pacman -F traceroute
extra/traceroute 2.1.6-1, usr/bin/traceroute

apt search lists matching packages by name on Debian and Ubuntu, and the package management chapter covers that kind of searching. Finding which package owns a file is the lesser-known half of the job, and the chapters leave it to this guide: Fedora’s dnf provides takes a path pattern and names the package that owns the file. Arch keeps a file database, which pacman -Fy refreshes before pacman -F traceroute prints the line above.

It might be in the wrong directory

Scripts are the classic trap. Bash looks for bare names along PATH, never in the directory you are standing in, so the script next to you is invisible until you point at it:

$ script.sh
bash: script.sh: command not found
$ ./script.sh
hello-from-vfyscript

The ./ prefix means “here, explicitly”. For a program installed somewhere unusual, the same logic applies to PATH itself:

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
$ PATH=$PATH:/opt/vfytools

That is Ubuntu’s login default, with Fedora and Arch each adding their own entries at the edges. The assignment reads left to right: the old PATH, a colon, the new directory, and afterwards tools in /opt/vfytools run by bare name. The shell essentials chapter introduces $PATH among the environment variables.

An alias can produce the same confusion from inside the shell itself: if a command works for a colleague but not for you, it may be an alias in their setup, and type NAME says so. The same chapter teaches aliases with the familiar ll example.

One more look-alike to close on. If bash had answered bash: ./script.sh: Permission denied, nothing in this section applies — that is the next failure, and beginners mix the two up for years.

“Permission denied”

Here is that look-alike in full:

$ ./script.sh
bash: ./script.sh: Permission denied
$ echo $?
126

The exit code sorts the pair: 127 meant “no such command”, 126 means “found it, not allowed to run it”. The file exists and is readable. The execute bit is off, and one chmod +x script.sh later the permission string has changed from -rw-r--r-- to -rwxr-xr-x and the script runs. (You may see a trailing character after those bits — + on Ubuntu when access lists are attached, . on Fedora for SELinux contexts. Ignore it for this purpose.) The permissions chapter teaches the numeric and symbolic forms.

Directories complicate the picture, because their two important bits do different jobs: r lets you list names, x lets you enter. “No permissions, no access” is the summary everyone learns, and then the verified split arrives. Both halves are one chmod away on a scratch directory: chmod 644 somedir is the r-without-x case. With r but no x:

$ cd /tmp/vfydir
bash: cd: /tmp/vfydir: Permission denied
$ ls /tmp/vfydir
vfyfile
$ ls -l /tmp/vfydir
ls: cannot access '/tmp/vfydir/vfyfile': Permission denied

You can read the names off the door but not walk in. Plain ls lists them, ls -l needs to open each file for its details and fails per entry. The mirror case (chmod 311 somedir takes the r away and leaves the x) flips the behavior: cd sails straight through while the listing dies:

ls: cannot open directory '/tmp/vfydir': Permission denied

The two bits are two questions: may I read the list, and may I pass the door.

The last permission failure is not about bits at all but about identity. Before reaching for sudo, check who the shell thinks you are:

$ id
uid=1000(user) gid=1000(user) groups=1000(user),27(sudo)
$ chown alice /tmp/vfyown.txt
chown: changing ownership of '/tmp/vfyown.txt': Operation not permitted

That chown error is what “you lack the authority” looks like when stated plainly — ownership changes need root, which is why sudo chown exists. (Ubuntu’s version of the error appends (os error 1).) A good share of permission errors turn out to be wrong-user errors, and id costs nothing to run.

“No space left on device”

The kernel does not negotiate about disk space. The error arrives for any write, from any program, and the first question is whether it is literally true:

$ df -h /mnt/vfyfs | tail -1
tmpfs            64M   64M     0 100% /mnt/vfyfs

(From the test rig: a 64M tmpfs, RAM posing as a disk, filled on purpose.) df -h reports bytes. When Use% reads 100, the next question is who ate them:

$ du -xh --max-depth=1 / 2>/dev/null | sort -rh | head -5
1.9G	/
1.3G	/usr
536M	/var
4.8M	/etc
64K	/root

Read it in parts. du measures per directory, sort -rh sorts the human-readable sizes biggest first (the pair the disks chapter teaches), and head -5 keeps the five biggest suspects. The -x pins du to one filesystem, and 2>/dev/null throws away the permission complaints about other people’s directories.

Sometimes df -h says there is plenty of space and writes fail anyway. Then the filesystem’s second budget is the culprit: every file needs an inode, an entry in the table of contents, and inodes can run out while bytes remain:

$ df -i /mnt/vfyino | tail -1
tmpfs             100   100     0  100% /mnt/vfyino
$ touch /mnt/vfyino/overflow
touch: cannot touch '/mnt/vfyino/overflow': No space left on device

Same error text, opposite cause — on this filesystem df -h showed 0% used. Millions of tiny files, the cache-directory kind, spend inodes without moving the byte counter much, and df -i tells the two shortages apart.

On a full root disk, the systemd journal is a frequent offender, and it will shrink itself on request:

$ journalctl --disk-usage
Archived and active journals take up 40M in the file system.

(40M on our Ubuntu container, 32M on Fedora, 16M on Arch — your number will differ.) Shrinking it is root territory:

$ sudo journalctl --vacuum-size=100M
Deleted archived journal /var/log/journal/…/system@….journal (70.1M).
Vacuuming done, freed 70.1M of archived journals from /var/log/journal/….

(The 70.1M came from a test rig that had grown well past its cap before the vacuum ran — a normally capped journal frees less. Your numbers will differ; the mechanism is the same.) Three honest limits, all verified. Only archived journals get deleted; the active file keeps collecting. The journal never shrinks to zero. And under the limit, the command prints freed 0B and changes nothing.

To reproduce any of this deliberately, the filler of choice is head -c 100M /dev/zero > filler.bin. The -c flag counts bytes, and the suffixes are powers of 1024 — 1M is 1048576 bytes, 1G is 1073741824. The drill at the end of this guide uses it.

“The network is down”

A network is a stack of questions, and “the internet is broken” is too big to answer. Walk them in order, from the machine outward. The networking chapter teaches each command in full — here they become a bisection.

  1. Is the TCP/IP machinery alive at all? ping -c 1 localhost should end in 1 packets transmitted, 1 received, 0% packet loss, time 0ms. This never touches a cable — failure here means the problem is inside the machine.

  2. Does the interface have an address? ip addr: look for your network device and an inet line beneath it.

  3. Is there a route out? ip route should contain a line that starts with default:

    default via 10.40.33.1 dev eth0 proto dhcp src 10.40.33.227 metric 100
    

    The default line names the gateway (via) and the device it lives on. Device names can differ between machines. Ours happened to say eth0 everywhere, but yours may say enp3s0, wlan0, anything.

  4. Can we reach the world by raw address? ping -c 3 1.1.1.1 ending in 3 packets transmitted, 3 received, 0% packet loss, time 2002ms means packets leave and come back.

  5. Do names resolve? This is where the classic split happens:

    $ dig example.com +short
    104.20.23.154
    172.66.147.243
    

    If the raw-address ping worked but the name fails with

    ping: example.com: Temporary failure in name resolution
    

    then the network is fine and DNS is the patient — that failure exits with code 2. dig @1.1.1.1 example.com +short asks a specific resolver instead of the system default, separating “our resolver is broken” from “the upstream is”. If dig is not on your machine, the ping split above already names the layer.

  6. Is anything listening? For “the service is unreachable” complaints, ss -tlnp shows who waits on which address:

    LISTEN 0      5            0.0.0.0:8822      0.0.0.0:*    users:(("python3",pid=88555,fd=3))
    LISTEN 0      5          127.0.0.1:8811      0.0.0.0:*    users:(("python3",pid=88554,fd=3))
    

    Two test servers, one mystery solved. The first listens on 0.0.0.0, every interface, reachable from outside. The second listens on 127.0.0.1, localhost only, invisible from anywhere else. A remarkable share of “it works on the machine but not remotely” tickets end at this line.

  7. Ping can lie. Some networks drop it while serving traffic happily, so test the real protocol instead. curl -I example.com wants to answer HTTP/2 200: headers only, no page body.

If raw pings die partway, traceroute shows the last hop that answered — the networking chapter owns it. And when ss says 0.0.0.0 but outsiders get nothing, the suspect becomes a firewall. The networking chapter sketches that landscape (ufw on Ubuntu, firewalld on Fedora) in prose without commands, which is roughly where this guide stops too.

“The service won’t start”

Services fail more politely than disks — systemd writes everything down. First the names, because they bite. The SSH daemon is ssh.service on Ubuntu, where sshd.service also exists as an alias, and sshd.service on Fedora and Arch. A good share of “unit not found” panic is one distro’s name typed on another distro’s machine. systemctl is-active NAME answers with a state, and inactive is a valid one: ssh sat disabled on our Ubuntu container, switched off rather than broken. The services and logs chapter teaches the systemctl basics.

systemctl --failed lists the sick units. A healthy machine answers 0 loaded units listed. A sick one names names — ● vfyfail.service loaded failed failed VFY broken unit — and closes with 1 loaded units listed.

For the story behind a failure, status first, journal second:

× vfyfail.service - VFY broken unit
     Active: failed (Result: exit-code) since Mon 2026-08-31 21:17:35 UTC; 31s ago

Status tells you that it failed. The journal tells you why:

$ journalctl -u vfyfail -n 20
vfyfail.service: Unable to locate executable '/nonexistent/vfyfail': No such file or directory
vfyfail.service: Failed at step EXEC spawning /nonexistent/vfyfail: No such file or directory
vfyfail.service: Main process exited, code=exited, status=203/EXEC
vfyfail.service: Failed with result 'exit-code'.

This unit pointed its ExecStart at a binary that does not exist, and status=203/EXEC is systemd’s shorthand for that. The pattern generalizes to any service: journalctl -u NAME -n 20 is where the reason lives. The SSH hardening guide reads the same lines for its own daemon.

One reload rule, learned the hard way. A new unit file is noticed on its own — drop it in place and start it, no ceremony needed. But after editing an existing one, systemd keeps running the stale version until you reload, and it says so right in the status output:

Warning: The unit file, source configuration file or drop-ins of vfyfail.service changed on disk. Run 'systemctl daemon-reload' to reload units.

The fix is written into the warning: run systemctl daemon-reload, then start again. The expensive version of this mistake is editing a unit, restarting the service, and testing the old configuration while believing you tested the new one.

“The system is slow”

“Slow” is a feeling. Two numbers turn it into a diagnosis.

Load first. uptime prints three averages, for the last 1, 5 and 15 minutes, and nproc prints the number of cores they get spread across:

$ uptime
 21:12:09 up 2 days, 13 min,  0 users,  load average: 0.69, 0.84, 0.89
$ nproc
12

Compare the biggest load number with the core count — 12 here, yours will differ. Load 0.89 on twelve cores is a machine twiddling its thumbs. The same 0.89 on a four-core laptop is a normal workday, and 12.0 on either means work is standing in line.

Memory second:

$ free -h
total used free shared buff/cache available
Mem:  31Gi  30Mi  31Gi  144Ki  176Mi  31Gi

Read the available column, not free. Linux parks spare RAM in buff/cache and hands it back the moment a program asks, so a scary free next to a healthy available is normal, not a problem. (These numbers come from a just-booted container; yours will look busier.)

Then find who is guilty:

$ ps aux --sort=-%mem | head

The memory hogs first, biggest at the top. top does the same thing live — press M to sort by memory, P by CPU, q to leave — and the processes chapter gives it a full tour.

When one process is the problem, pgrep -x firefox prints the IDs of processes whose name matches exactly. kill PID asks it to exit. kill -9 PID forces the issue when nothing else works — a last resort, as the processes chapter explains.

Logs after the fact

The services and logs chapter introduces journalctl with -n, -f and a first --since. Reading yesterday’s failure wants three more filters, and they combine freely:

$ journalctl --since "1 hour ago"
$ journalctl --since today
$ journalctl -p err
$ journalctl -b

Both --since forms work as written. -p err is the one that surprises people: it shows err-level entries and everything more severe. In our probe run, logs written at crit appeared and logs at info stayed hidden — it is a floor, not a filter for that level alone. -b cuts the output to the current boot, which after a month of uptime is the difference between two hundred lines and two hundred thousand.

You can watch the severity filter work on your own entries, because logger writes into the journal. A plain logger "vfy no-tag message" arrived in our capture as root[2431]: vfy no-tag message. The bracketed tag is your username, picked for you when you do not pass -t. Entries written at user.err and user.crit pass a -p err reading, ordinary ones do not.

One honesty note about access. A fresh, unprivileged user on Ubuntu and Fedora sees only their own user-journal entries — no error message, just a shorter list. On Arch the same user gets

No journal files were opened due to insufficient permissions.

Membership in the right group opens the whole system journal: adm on Ubuntu, wheel or systemd-journal on Fedora and Arch. Through sudo you always see everything.

The disk failure, start to finish

Everything from the disk section in one transcript, run for real in a disposable container — a small tmpfs standing in for a full disk, so the failure could be triggered, measured and cleaned up without anyone’s actual drive at risk:

$ mkdir -p /mnt/vfyfs && sudo mount -t tmpfs -o size=64M tmpfs /mnt/vfyfs
$ df -h /mnt/vfyfs | tail -1
tmpfs            64M     0   64M   0% /mnt/vfyfs
$ head -c 100M /dev/zero > /mnt/vfyfs/filler.bin
head: error writing 'standard output': No space left on device
$ df -h /mnt/vfyfs | tail -1
tmpfs            64M   64M     0 100% /mnt/vfyfs
$ du -xh --max-depth=1 /mnt/vfyfs
64M	/mnt/vfyfs
$ du -xh --max-depth=1 / 2>/dev/null | grep mnt
0	/mnt
$ du -h --max-depth=1 / 2>/dev/null | grep mnt
64M	/mnt
$ rm /mnt/vfyfs/filler.bin && df -h /mnt/vfyfs | tail -1
tmpfs            64M     0   64M   0% /mnt/vfyfs

Walk it through. The fresh mount reports 64M available, nothing used. head -c 100M asks for a hundred mebibytes of zero bytes, the filesystem has 64M, and head dies at the wall with the error you now recognize. (Ubuntu appends (os error 28) to that line — the form above is the Fedora and Arch shape.) df confirms the wall: 64M used, 0 available, 100%.

The du pairs are the -x lesson, caught live. From inside the tmpfs, du counts 64M. From the root with -x, the mountpoint /mnt counts as 0, because -x refuses to cross into another filesystem. That is why the drill uses it: the numbers stay attributable to the disk that owns them. Drop the -x and the tmpfs’s 64M shows up under /mnt, billed to the wrong disk.

The last line is the payoff. Remove the filler and the space comes back: 0 used, 64M available, 0%.

For trying this at home, the tmpfs route is the recommended one: run the mount command from the transcript and you are filling RAM that poses as a disk, so nothing real is at risk, everything in it is gone at the next reboot, and the only price is sudo for the mount step. The no-mount variant, where the filler points at a directory on a filesystem you want to watch instead, is for a real disk only if you know exactly why you are doing it.

Where you are now

One habit carries everything above: what did it say exactly, what changed since it worked, and what is the smallest test that still fails. The chapters hold the tool reference — processes and system for top, free and friends, services and logs for systemctl and journalctl in full.

Three links worth keeping, all in English:

The Arch Wiki page is the best long-form map of this territory, and the man pages answer the questions this guide deliberately skipped.