The bash wait command solves a specific Linux scripting problem: a script can start work in the background, but it still needs a reliable point where those jobs have finished and their exit statuses are known. Use wait when fixed delays with sleep would be guesswork, especially in backup, test, conversion, and maintenance scripts that run several child processes in parallel.
On Linux, wait is normally a shell built-in, not a standalone binary. Bash adds useful options such as -n, -p, and -f, while POSIX sh keeps a smaller portable form for waiting on known child process IDs.
Understand the Bash wait Command
The GNU Bash manual documents wait as a command that waits for child processes or shell jobs and returns the termination status of the relevant job. The child-process boundary matters: Bash can wait for commands started by the current shell, but it cannot collect the exit status of a random PID from another terminal or service manager.
Start a command in the background with &, save its PID from $!, then wait for that PID when the script needs the result. That pattern is the safest foundation because the script keeps an explicit handle for each background job.
Bash wait Syntax
wait [-fn] [-p variable] [id ...]
| Form | Purpose | Exit status behavior |
|---|---|---|
wait | Wait for all active child processes started by the current shell | Returns 0 after all current children finish |
wait "$pid" | Wait for one saved process ID | Returns that child process’s exit status |
wait "$pid1" "$pid2" | Wait for several specific process IDs | Returns the status of the last ID in the list |
wait -n | Return when the next background job finishes | Returns the status of the job that finished first |
wait -n -p done_id | Store the PID or jobspec that satisfied wait -n | Useful for reporting or cleanup after the first completed job |
wait -f "$pid" | With job control enabled, wait for termination instead of a status change | Most relevant in interactive shells or scripts that enable job control |
Bash accepts process IDs, such as 12345, and job specifications, such as %1. Job specifications come from the current shell’s job table, so they are common in interactive sessions but less predictable in scripts than saved PIDs.
Verify wait Is a Bash Built-in
Use type to confirm that the current shell resolves wait as a built-in command. A Bash built-in has no separate executable path because Bash handles the child-process state internally.
type wait
wait is a shell builtin
For Bash-specific syntax and option support, use the shell’s built-in help instead of searching for an external man page:
help wait | sed -n '1,2p'
wait: wait [-fn] [-p var] [id ...]
Wait for job completion and return exit status.
wait vs sleep in Bash Scripts
wait and sleep both pause execution, but they answer different questions. sleep 10 asks Bash to pause for ten seconds. wait "$pid" asks Bash to pause until a specific child process exits, whether that takes one second or one hour.
| Need | Use | Reason |
|---|---|---|
| Synchronize with a background task | wait | The script resumes only when the child process exits |
| Read the child process exit code | wait "$pid" | Bash returns the waited-for command’s status |
| Pause between retries | sleep | No background job has to finish first |
| Rate-limit a loop | sleep | The delay is the point, not process synchronization |
Wait for a Background Process by PID
The $! variable expands to the PID of the most recent background job. Save it immediately after starting the command; another background command will overwrite the value.
#!/usr/bin/env bash
sleep 2 &
process_id=$!
echo "Started background process: $process_id"
if wait "$process_id"; then
echo "Process finished successfully."
else
status=$?
echo "Process failed with status: $status"
fi
The printed PID changes on every run, but the success path should look like this:
Started background process: 2514821 Process finished successfully.
Use wait "$process_id" instead of a fixed sleep delay when the task length varies. The script receives the real exit status and avoids continuing too early or wasting time after the job has already finished.
Wait for All Background Jobs
Running wait with no IDs waits for all active child processes from the current shell. This compact form is useful when the script only needs a barrier before the next step and does not need per-job status reporting.
#!/usr/bin/env bash
sleep 1 &
sleep 2 &
sleep 3 &
echo "Waiting for all background jobs..."
wait
echo "Every background job has finished."
Waiting for all background jobs... Every background job has finished.
This form returns 0 after all current children finish, even if one child failed earlier and another succeeded later. Use saved PIDs and individual waits when failures must be reported accurately.
Capture Exit Codes from Multiple Jobs
A single wait "$pid1" "$pid2" "$pid3" command waits for every listed PID, but Bash returns the status of the last ID in that list. That can hide an earlier failure, so production scripts should usually wait for each saved PID and record the status immediately.
#!/usr/bin/env bash
pids=()
bash -c 'sleep 1; exit 0' &
pids+=("$!")
bash -c 'sleep 2; exit 4' &
pids+=("$!")
failed=0
for pid in "${pids[@]}"; do
if wait "$pid"; then
echo "Process $pid finished successfully."
else
status=$?
echo "Process $pid failed with status $status."
failed=1
fi
done
if ((failed)); then
echo "At least one background job failed."
else
echo "All background jobs finished successfully."
fi
Process 2514934 finished successfully. Process 2514935 failed with status 4. At least one background job failed.
Avoid
if ! wait "$pid"; then echo "$?"; fiwhen you need the child’s real status. The!operator inverts the command result, so$?inside that branch becomes the status of the inversion, not the original child process.
Reference Interactive Jobs with wait
Interactive shells can refer to background jobs by job specification. Start a job, inspect it with jobs -l, then wait for the job number shown in brackets.
sleep 60 &
jobs -l
wait %1
[1]+ 2514978 Running sleep 60 &
Job specifications are shell-local. A %1 value from one terminal does not identify a job in another terminal, and it can change as jobs finish or new jobs start. Scripts are more reliable when they save PIDs in variables or arrays.
Use wait -n for the First Completed Job
The -n option returns as soon as one unwaited-for child process exits. Bash added wait -n in Bash 4.3, so scripts that must run on older enterprise systems need a version check or a simpler PID loop.
#!/usr/bin/env bash
bash -c 'sleep 2; exit 0' &
fast_pid=$!
bash -c 'sleep 4; exit 0' &
slow_pid=$!
if wait -n -p finished_pid "$fast_pid" "$slow_pid"; then
status=0
else
status=$?
fi
echo "First completed PID: $finished_pid"
echo "Exit status: $status"
for pid in "$fast_pid" "$slow_pid"; do
if [[ $pid != "$finished_pid" ]]; then
wait "$pid"
fi
done
First completed PID: 2515036 Exit status: 0
The -p option stores the PID or jobspec that satisfied wait -n in the named variable. Bash added -p in Bash 5.1, and it is useful only with -n because normal wait already knows which explicit PID it is waiting for.
Check the installed Bash version before using newer options in scripts that run across many Linux distributions:
bash --version | head -n 1
GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu)
Limit Parallel Jobs with wait -n
Unbounded background loops can overload CPU, memory, disk, or remote services. Use wait -n as a simple throttle: when the script reaches the job limit, it waits for one job to finish before starting another.
#!/usr/bin/env bash
max_jobs=2
running=0
for item in alpha beta gamma delta; do
if ((running >= max_jobs)); then
wait -n
((running -= 1))
fi
bash -c 'sleep 1; printf "%s done\n" "$1"' _ "$item" &
((running += 1))
done
wait
echo "All queued work finished."
The worker lines can appear in a different order because two jobs run at the same time. One possible run looks like this, with the final line appearing only after the last queued job finishes:
alpha done beta done gamma done delta done All queued work finished.
For file-driven work, combine this pattern with a safe file discovery step. For example, find with -exec can locate matching paths, while a Bash loop controls how many worker processes run at once.
Add a Timeout Around wait
Bash wait has no built-in timeout option. In Bash 5.1 or newer, a common pattern is to start the real task and a timer process, then use wait -n -p to see which one finished first.
#!/usr/bin/env bash
sleep 5 &
task_pid=$!
sleep 2 &
timer_pid=$!
if wait -n -p finished_pid "$task_pid" "$timer_pid"; then
status=0
else
status=$?
fi
if [[ $finished_pid == "$timer_pid" ]]; then
echo "Task timed out."
kill "$task_pid" 2>/dev/null
wait "$task_pid" 2>/dev/null || true
exit_status=124
else
kill "$timer_pid" 2>/dev/null
wait "$timer_pid" 2>/dev/null || true
echo "Task exited with status $status."
exit_status=$status
fi
exit "$exit_status"
Task timed out.
The timeout branch exits with status 124 so a caller can distinguish a deadline from a successful task. If the command is a single foreground command rather than a broader background-job workflow, the GNU timeout utility is often simpler. Use the timer-plus-wait pattern when the script already manages child PIDs and needs cleanup logic around them.
Use wait -f with Job Control Status Changes
The -f option changes behavior only when Bash job control is enabled. Without -f, wait can return when a job changes status, such as when it is stopped with SIGSTOP. With -f, Bash waits for termination instead.
set -m
sleep 300 &
pid=$!
(sleep 1; kill -STOP "$pid") &
controller_pid=$!
wait "$pid"
echo "wait returned after the stop signal."
kill -CONT "$pid"
kill "$pid"
wait "$pid" 2>/dev/null || true
wait "$controller_pid" 2>/dev/null || true
wait returned after the stop signal.
With -f, Bash keeps waiting until the process terminates. This example stops the job, resumes it after one second, then terminates it so the demonstration cleans itself up:
set -m
sleep 300 &
pid=$!
(sleep 1; kill -STOP "$pid"; sleep 1; kill -CONT "$pid"; sleep 1; kill -TERM "$pid") &
controller_pid=$!
if wait -f "$pid"; then
status=0
else
status=$?
fi
echo "wait -f returned after termination with status $status."
wait "$controller_pid" 2>/dev/null || true
wait -f returned after termination with status 143.
The status 143 in this example is the shell’s signal-style status for a process terminated by SIGTERM, calculated as 128 plus signal number 15.
In non-interactive scripts, job control is disabled by default, so -f rarely changes normal script behavior. It is mainly useful for interactive job-control sessions or scripts that explicitly enable job control with set -m.
Use Portable wait in POSIX sh
The POSIX wait utility supports waiting for known process IDs or all known child processes, but it does not define Bash-specific options such as -n, -p, or -f. Portable sh scripts should avoid Bash arrays, [[ ... ]], and wait -n.
#!/bin/sh
sleep 1 &
pid1=$!
sleep 2 &
pid2=$!
wait "$pid1"
status1=$?
wait "$pid2"
status2=$?
printf 'First status: %s\n' "$status1"
printf 'Second status: %s\n' "$status2"
This portable form works across Linux distributions that link /bin/sh to shells such as dash, Bash in POSIX mode, or another POSIX-compatible shell. Use #!/usr/bin/env bash when a script depends on Bash-only features.
Troubleshoot Common wait Command Errors
Most wait failures come from the same root causes: the target PID is not a child of the current shell, the shell is too old for the option, or the script lost the real exit status before saving it.
wait Reports That the PID Is Not a Child
Bash returns status 127 when the PID is invalid or not a child process of the current shell. A typical error looks like this:
bash: wait: pid 999999 is not a child of this shell
Use wait only for processes started by the same shell. To monitor a non-child PID, poll for existence with kill -0, but remember that this only tells you whether the process still exists; it cannot recover the process’s exit code.
while kill -0 "$pid" 2>/dev/null; do
sleep 1
done
echo "Process $pid no longer exists."
wait -n or wait -p Returns invalid option
wait -n requires Bash 4.3 or newer, and wait -p requires Bash 5.1 or newer. POSIX sh also rejects these options, even on systems where Bash is installed.
bash --version | head -n 1
GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu)
If the script starts with #!/bin/sh, changing the command line is not enough. Either rewrite the script to use portable sequential waits, or change the shebang to Bash when Bash-only options are required.
A Subshell or Pipeline Lost the Child PID
A background job started inside a subshell belongs to that subshell. The parent shell cannot wait for a child it did not start directly.
# The parent shell cannot wait for this background job.
pid=$(bash -c 'sleep 10 & echo "$!"')
wait "$pid"
kill "$pid" 2>/dev/null || true
Launch the background job in the current shell when the parent needs to wait for it:
sleep 10 &
pid=$!
wait "$pid"
The Script Prints the Wrong Failure Code
Save $? immediately after wait fails. Another command, including echo, a test, or the ! negation operator, can replace the status before you print it.
if wait "$pid"; then
status=0
else
status=$?
fi
echo "wait status: $status"
jobs and ps Do Not Show the Same Processes
jobs -l shows the current shell’s job table, while ps --ppid $$ shows processes whose parent PID is the current shell. They are useful diagnostics, but they answer slightly different questions.
jobs -l
ps --ppid $$
kill -0 "$pid" 2>/dev/null && echo "Running" || echo "Gone"
Pipe ps output through the grep command only when you need to filter by process name. If the PID never appears as a child of the current shell, wait cannot collect its exit status.
When Not to Use wait
wait is small and reliable when the current shell owns the child processes. Other tools fit better when the job queue, scheduler, or supervisor needs features that Bash does not provide.
| Scenario | Better tool | Reason |
|---|---|---|
| Large queues with retries, progress, and argument replacement | GNU Parallel or xargs -P | Purpose-built parallel runners handle queue control better than hand-written PID loops |
| Jobs that should start when system load drops | batch command in Linux | The job belongs to the system queue, not the current shell session |
| Long-running services that should restart after failure | systemd or another supervisor | Service managers own restart policy, logs, dependencies, and boot behavior |
| Waiting for a process started elsewhere | kill -0, service status checks, logs, or IPC | wait cannot collect exit status from non-child processes |
| Sequential scripts with no background jobs | No synchronization command | Foreground commands already finish before the next line runs |
Conclusion
Background work is synchronized when each child PID is saved, waited for once, and checked before the script continues. Use basic wait for portable shell scripts, wait -n and -p for responsive Bash job handling, and external queue or supervisor tools when the workflow outgrows the current shell.


Formatting tips for your comment
You can use basic HTML to format your comment. Useful tags currently allowed in published comments:
<code>command</code>command<strong>bold</strong><em>italic</em><blockquote>quote</blockquote>