SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Fix: Argument list too long in Linux

Argument list too long is the kernel refusing an oversized exec, and the glob you typed is usually why. Reproduce it, then fix it with xargs or find.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 28, 2026.

What "Argument list too long" means

Argument list too long is the kernel refusing to start a program, because the argument list that program would receive is larger than the kernel is willing to copy. The command never ran. Nothing was deleted and nothing was written, so you can rewrite the line and try again safely.

Every external command ends in one execve call, the system call that loads a new program into a process. Before that call, bash builds two arrays: the argument list (argv) and the environment (envp). execve copies both into the memory of the new program before its first instruction runs. The kernel caps the size of that copy. When the copy would be too big, execve fails with the error code E2BIG, and bash turns that code into the message you searched for.

The message names the program bash resolved on your PATH, not the word you typed:

bash: /usr/bin/rm: Argument list too long

Because the check happens inside execve, no shell syntax moves it. Quoting the pattern does not help. Running the same line under zsh or dash does not help, because every shell on Linux ends at the same system call and meets the same ceiling. This is not a bug in bash and it is not a bug in rm.

Everything in this guide uses coreutils and findutils, which are installed on every Ubuntu image. There is nothing to add.

Why a glob is almost always the trigger

The shell expands *.log before rm starts. rm never sees a star. In a directory holding a hundred thousand files, rm *.log is not one argument: bash replaces the pattern with one argument per matching name, then calls execve with the whole list. The command that fails is a command you never typed. If that expansion order is new to you, how bash expands globs before the command ever runs is the background for everything below.

Shell builtins never call execve, so they never meet the ceiling. echo *.log works in the same directory where ls *.log fails, because echo is built into bash while ls is a program on disk. That difference is your first diagnostic. If swapping the command for a builtin makes the error disappear, the size of the expansion is the problem, not the files.

Reproduce it in a scratch directory

Build a directory with enough entries that the expansion cannot fit.

mkdir -p /tmp/argmax-demo
cd /tmp/argmax-demo
seq -f 'application-server-%08g.log' 1 200000 | xargs touch

seq -f prints one formatted name per line. xargs reads those lines and calls touch in batches small enough to run, which is the same fix you will use later in this guide. Creating the files takes a few seconds.

Now measure the expansion with a builtin, and print the limit your C library reports:

echo *.log | wc -c
getconf ARG_MAX

echo is a builtin, so the first line survives the expansion that breaks everything else. wc -c receives the list on standard input rather than as arguments, so it is safe too, and it prints the size of the expanded list in bytes. getconf ARG_MAX prints the limit. If the first number is not clearly larger than the second, delete the directory, raise the count in the seq line, and measure again. Once the first number is larger, the failure is one command away:

rm *.log
ls *.log | wc -l

Both fail with the same message, because both are programs on disk receiving the same expanded list.

Why is the practical ceiling lower than getconf ARG_MAX?

getconf ARG_MAX prints a single number, and the kernel checks more than one thing, so treat that number as an upper bound you will never actually reach.

The environment is copied by the same execve call, and it shares the same budget. A shell carrying a large environment fails earlier than a shell carrying a small one. Print the size of yours with env | wc -c, then compare these two lines:

xargs --show-limits < /dev/null
env -i /usr/bin/xargs --show-limits < /dev/null

xargs prints its own accounting, including a line labelled Maximum length of command we could actually use. That value is the honest one, because xargs has already subtracted the environment it is holding. The second line starts xargs with an empty environment, so watch that same value rise.

Two more reasons the real ceiling sits under the printed one. Each argument is stored as a string with a terminating NUL byte (the zero byte) plus a pointer in an array, so a list of many short names costs more than the characters you can count. And there is a second cap on the length of any single argument, separate from the total, which spare room in the total will not satisfy.

Watch that single-argument cap on its own:

big=$(head -c 300000 /dev/zero | tr '\0' 'a')
echo "$big" | wc -c
/usr/bin/echo "$big" > /dev/null

The builtin echo prints the size without complaint. The program /usr/bin/echo fails with Argument list too long on one argument. Compare the size wc -c printed against what getconf ARG_MAX reported: the total had room to spare. A single oversized string is enough on its own.

Finally, the ceiling is derived from the stack size limit of the process. Lower ulimit -s in a throwaway shell, run getconf ARG_MAX again, and watch the reported number move with it. There is no correct value to memorise, because it is a property of the shell you are sitting in.

Fix 1: pipe the names into xargs

find . -maxdepth 1 -name '*.log' -print0 | xargs -0 rm -v

find writes the names into a pipe, and a pipe has no argument limit. xargs reads them and calls rm as many times as it needs to, each call carrying a batch that fits. -print0 separates names with a NUL byte and -0 tells xargs to expect that separator, which is the one byte that cannot appear inside a filename. Quote the -name pattern. Leave it bare and the shell expands it before find starts, which produces find: paths must precede expression instead.

-maxdepth 1 keeps find in the current directory. Without it, find walks every subdirectory, which is a much larger delete than the glob you were trying to run.

Fix 2: let find run the command with -exec and a plus sign

find . -maxdepth 1 -name '*.log' -exec rm -v {} +

The trailing + tells find to pack as many names as fit into each rm call. That is the same batching xargs does, without the pipe. The older form -exec rm {} \; is also correct, but it starts one rm process per file, so on a hundred thousand files it is slow enough to notice. Use + unless the command truly handles one file at a time.

Fix 3: find -delete, which never execs at all

find . -maxdepth 1 -name '*.log' -delete

-delete is an action inside find, so find calls unlinkat itself. No new process starts, so there is no argument list that could be too long. Put -delete at the end of the expression. find evaluates left to right, so find . -delete -name '*.log' deletes everything it reaches before it looks at a single name.

Fix 4: a shell loop, so the list never becomes one argv

find . -maxdepth 1 -name '*.log' -print0 | while IFS= read -r -d '' f; do
  rm -- "$f"
done

The loop calls rm once per file, so each execve carries one name. IFS= stops read from trimming leading spaces and -r stops it treating a backslash as an escape. -d '' makes read split on the NUL byte that -print0 wrote. The -- marks the end of options, so a file named -rf is handled as a name instead of a flag.

A plain glob loop also works, and the reason is worth holding on to: for f in ./*.log; do rm -- "$f"; done expands the whole pattern inside bash, which has no such limit, and then calls rm once per name. The shell can hold a word list far larger than it can hand to a program.

Deleting a huge directory safely

Preview before you delete. Run the exact find you intend to use, with the action removed:

find . -maxdepth 1 -name '*.log' | wc -l
find . -maxdepth 1 -name '*.log' | head

The count tells you how much you are about to lose and head shows you the shape of the names. If either one surprises you, the pattern is wrong. Files removed from ext4 or xfs are not recoverable by ordinary means, and what recovery after an rm -rf actually involves is worth reading before you need it. The preview costs a second. The recovery costs a day.

If the whole directory is going, you never needed the glob:

cd ..
rm -rf ./argmax-demo

That is one argument however many files are inside, because rm -rf walks the tree itself and never builds a list in the shell. The number of files on disk was never what broke the command. What broke it was the number of words the shell put on one command line.

Where else this error shows up

The same ceiling catches any command you feed from a glob or a substitution.

  • cp *.jpg /backup/ fails on a large directory. Use find . -maxdepth 1 -name '*.jpg' -print0 | xargs -0 cp -t /backup/, where -t names the destination first so xargs can append the filenames after it.
  • grep pattern *.log fails. Use grep -r --include='*.log' pattern . so grep walks the directory itself instead of the shell.
  • rm $(cat filelist.txt) fails once the list is long, because command substitution pastes its output onto the command line before the command starts. Use xargs -a filelist.txt rm instead.
  • ./cleanup.sh *.log fails, because running a script with ./ starts a new process through execve and its arguments go through the same copy. Sourcing the script with . runs it in the current shell with no exec at all, and the difference between sourcing and executing a script covers what else that changes.

The messages you will see, and what each one means

bash: /usr/bin/rm: Argument list too long means the program never started. Nothing was removed, so the directory is exactly as it was. Rewrite the line with find and run it again.

find: paths must precede expression, followed by one of your filenames, means you left the -name pattern unquoted. The shell expanded it and handed find a list of paths where it expected a test. Put single quotes around the pattern.

xargs: unmatched single quote; by default quotes are special to xargs unless you use the -0 option means you piped plain text into xargs and a filename contained a quote character. Feed it find -print0 and read it with xargs -0.

find: missing argument to '-exec' means the -exec had no terminator. It needs a + at the end, or a \; with the semicolon escaped so the shell does not eat it.

rm: cannot remove 'my': No such file or directory after a ls | xargs rm means a filename containing a space was split into two arguments. ls output is text, and xargs split it on whitespace. This is the failure that -print0 and -0 exist to prevent.

FAQ

Why do I get "Argument list too long" when I have plenty of free memory?

The limit is not your free memory. execve copies the argument list and the environment into the new program before it starts, and the kernel caps the size of that copy. The cap is derived from the stack size limit of the calling process, so it stays the same whether the box has one gigabyte of RAM free or one hundred. Run getconf ARG_MAX and xargs --show-limits < /dev/null to see the reported limit and the usable one on your machine.

Does raising ulimit -s fix Argument list too long?

It moves the ceiling, and you can watch it move: change ulimit -s in a throwaway shell and run getconf ARG_MAX again in that same shell. It is a poor fix. The change applies only to that shell and its children, so a cron job or a script started elsewhere never sees it, and it does not lift the separate cap on the length of any single argument. Batching the command with xargs or find works everywhere and needs no privilege.

Can I delete the whole directory instead of matching a glob?

Yes, and it is the simplest answer when everything in the directory is going. rm -rf ./logdir from the parent directory passes one argument, so the number of files inside is irrelevant: rm walks the tree itself and removes entries as it goes. Recreate the directory afterwards with mkdir and reset its owner and mode if a service writes there.

Why does ls | xargs rm break on some filenames?

ls prints names as plain text and xargs splits that text on whitespace, so a file called my report.log arrives as two arguments and rm reports No such file or directory for each half. Quote characters in a name cause xargs: unmatched single quote for the same reason. Use find . -print0 | xargs -0 so the names are separated by a NUL byte, which cannot occur inside a filename, or skip the pipe with find ... -exec rm {} +.