The bash wait command pauses script execution until one or more background processes finish, then returns their exit status. When you run multiple tasks in parallel, such as file processing, backups, or batch media conversion, wait coordinates when each job completes so your script can proceed safely.
Effectively using wait means understanding process IDs, job specifications, exit statuses, and the pitfalls that come with coordinating parallel work in production scripts.
Understand the Bash wait Command
If you are new to managing background processes, think of wait as a checkpoint in your script that says “pause here until these jobs finish.” When you launch a command in the background using &, Bash gives you a process ID (PID) and immediately returns control so you can run more commands. The wait command lets you synchronize those parallel tasks, ensuring they complete before your script proceeds.
The wait command is a shell built-in that pauses script execution until specified background jobs finish, then returns their exit status. Since it’s built directly into Bash (and most other shells), it executes instantly without spawning external processes, making it ideal for process synchronization in scripts that spawn parallel work.
Wait Command Syntax Breakdown
The basic structure is straightforward:
wait [OPTIONS] [ID...]
- wait – The command itself (a Bash built-in, not an external program)
- OPTIONS (optional) – Flags like
-n(wait for any one job),-f(wait for termination, not just status change), or-p(store completed job ID in a variable) - ID (optional) – One or more process IDs (like
12345) or job specifications (like%1). If omitted, wait pauses until all background jobs complete.
At its simplest, sleep 10 & wait starts a 10-second timer in the background, then immediately waits for it to finish before continuing. This two-command pattern forms the foundation for wait usage.
When you pass one or more IDs, wait returns the exit status of the last ID in the list. If you omit IDs, wait pauses until all active child processes finish and returns 0.
Common Wait Tasks Quick Reference
| Task | Command | What It Does |
|---|---|---|
| Wait for all background jobs | wait | Pauses until every background process spawned by the current shell completes |
| Wait for specific process | wait 12345 | Pauses until PID 12345 finishes, returns its exit status |
| Wait for multiple processes | wait 123 456 789 | Pauses until all three PIDs complete, returns exit status of last one (789) |
| Wait for any one job to finish | wait -n | Returns as soon as the first background job completes (Bash 4.3+) |
| Capture which job completed | wait -n -p var | Stores the PID of the completed job in variable var (Bash 5.1+) |
| Wait for actual termination | wait -f 12345 | Ignores status changes like STOP signals, waits for process to truly end |
| Wait for job by number | wait %1 | Pauses until job number 1 (from jobs output) completes; replace 1 with the actual job ID your shell reports |
For instance, to pause for a background process with PID 7654, you would use:
wait 7654
wait vs sleep in Bash
The wait and sleep commands serve different purposes, and confusing them is one of the most common mistakes in shell scripting. The sleep command pauses execution for a fixed duration regardless of what other processes are doing. The wait command pauses until a specific background process finishes, regardless of how long that takes.
| Feature | wait | sleep |
|---|---|---|
| Purpose | Pause until a background process exits | Pause for a fixed time period |
| Trigger to resume | Target process terminates | Specified duration elapses |
| Returns exit status | Yes, the exit code of the waited-for process | Always 0 (unless interrupted) |
| Requires background job | Yes, operates on child processes | No, standalone delay |
| Example | command & wait $! | sleep 5 |
Use wait when you need to synchronize with a background job’s completion. Use sleep when you need a fixed delay, such as pausing between retry attempts or rate-limiting API calls.
Reference Background Jobs by ID with wait
Beyond process IDs, you can reference jobs using job specifications (jobspec) that begin with a percentage symbol followed by the job number (%n). This becomes useful when you’re working interactively or need to reference jobs by their shell-assigned numbers rather than tracking PIDs manually. Here’s an example:
rsync -a ~/Projects/ ~/backups/projects/ &
When you run this command in the background, the shell displays the job ID (in brackets) and the process ID. Check the current list with jobs, then reference it by job number:
wait %1
Use wait -n to Act on the First Completed Job
The -n option changes wait’s behavior to return as soon as any single job from the provided list completes, rather than waiting for all of them. This proves useful when you need to process results as they become available instead of blocking until everything finishes.
wait -n 45432 54346 76573
Bash added -n in version 4.3. Some long-term support distributions still ship Bash 4.2, so you may see an “invalid option” error until you upgrade.
If you omit PIDs and jobspecs,
wait -nreturns when any background job completes. It returns exit status 127 when no unwaited-for background jobs remain.
Check your Bash version with:
bash --version | head -n 1
Expect output similar to the following:
GNU bash, version 5.3.3(1)-release (x86_64-pc-linux-gnu)
This command returns the exit status of whichever job finishes first, but doesn’t tell you which one completed. To capture the PID or jobspec of the completed job, use the -p option to store it in a variable:
wait -p job_id -n 45432 54346 76573
echo "Completed job: $job_id"
The -p flag arrived in Bash 5.1. Older shells throw the same “invalid option” message, so use bash --version to confirm you are on Bash 5.1 or newer if your system reports an error.
Use wait -f to Block Until Processes End
The -f option forces wait to pause until each specified process actually terminates, rather than returning when the job status merely changes (like stopping or suspending). This matters when job control is enabled, typically in interactive shells but not in scripts by default.
Without -f, sending SIGSTOP to a background process causes wait to return immediately because the job’s status changed. With -f, the command continues blocking until the process truly exits:
sleep 3600 &
pid=$!
echo "PID: $pid"
wait -f "$pid"
echo "Process terminated with status: $?"
If another terminal sends kill -STOP $pid, the wait -f call ignores the status change and keeps blocking. The command only returns when the process receives a terminating signal like SIGTERM or SIGKILL.
The
-foption requires Bash 4.3 or newer and has no effect when job control is disabled (the default in non-interactive scripts). It’s most useful in interactive sessions or scripts that explicitly enable job control withset -m.
When to Use the Wait Command
Understanding when wait adds value helps you decide whether to use it in your scripts. The command excels in specific scenarios where parallel processing or background job coordination matters:
Backup and Sync Operations
When backing up multiple directories to different destinations simultaneously, wait ensures all transfers complete before marking the backup job finished. For instance, syncing home directories, databases, and configuration files to separate backup servers runs faster in parallel, but you need wait to confirm everything succeeded before sending a completion notification or unmounting backup drives.
This simple backup wrapper launches two rsync jobs in parallel, waits for each PID, and reports whether every server finished successfully:
#!/bin/bash
rsync -a /srv/files/ user@nas1:/backups/files/ &
pid_files=$!
rsync -a /srv/databases/ user@nas2:/backups/db/ &
pid_db=$!
echo "Waiting for rsync jobs to finish..."
if wait "$pid_files" && wait "$pid_db"; then
echo "Both backup targets finished successfully."
else
echo "One of the rsync jobs failed. Check logs for details."
fi
Because each wait call blocks on one PID, you get precise exit status information for each remote sync instead of hoping that a combined wait would reveal failures.
Waiting for rsync jobs to finish... Both backup targets finished successfully.
Batch File Processing
Converting media files, compressing logs, or generating thumbnails from images often processes independently across files. You can use find with -exec to locate files and then launch parallel processing jobs coordinated by wait. The exact speedup depends on CPU, disk, and I/O bottlenecks.
Deployment and Testing Workflows
CI/CD pipelines frequently run parallel test suites, deploy to multiple servers simultaneously, or execute independent build steps concurrently. Wait coordinates these operations, ensuring critical dependencies complete before dependent steps proceed. You might deploy application servers in parallel but wait for all of them before updating the load balancer configuration.
The example below kicks off unit tests and an integration test container in parallel, waits for both jobs, then proceeds with deployment steps only if every check passes:
#!/bin/bash
pytest tests/unit &
pid_unit=$!
docker compose run --rm integration-tests &
pid_integration=$!
echo "Waiting for CI jobs to finish..."
if wait "$pid_unit" && wait "$pid_integration"; then
./scripts/deploy.sh
else
echo "CI checks failed, aborting deployment."
exit 1
fi
This structure prevents a deployment from continuing until the script confirms that both the Python suite and the containerized integration suite reported success.
Waiting for CI jobs to finish... CI checks failed, aborting deployment.
System Maintenance Scripts
Nightly maintenance often includes multiple independent tasks like database optimization, log rotation, cache cleanup, and index rebuilding. Running these sequentially wastes time during maintenance windows. Launching them in parallel with wait ensures they all finish before services restart or the maintenance window closes.
Data Collection and Aggregation
Scripts that gather metrics from multiple servers, query several APIs simultaneously, or collect log data from distributed systems benefit from parallel execution. You can combine wait with the read command to parse collected output after all background jobs complete. This pattern appears frequently in monitoring dashboards and status reporting tools.
Practical Examples of the Wait Command
Understanding wait through real examples helps clarify how it coordinates background processes. The following script demonstrates the basic mechanics:
#!/bin/bash
sleep 30 &
process_id=$!
echo "PID: $process_id"
wait $process_id
echo "Exit status: $?"
Here’s what happens in this script:
- The shebang line tells the system to use Bash as the interpreter.
- The
sleepcommand runs in the background (indicated by&), simulating a long-running task. $!captures the PID of the most recent background job and stores it inprocess_id.- The script prints the PID, then blocks at the
waitline until the sleep process finishes. $?contains the exit status of the last command. In this case, that’s whatwaitreturns (the exit status of the sleep process).
If you run the script, it will print something like this:
PID: 36353 Exit status: 0
The exit status of 0 indicates successful completion. Any non-zero value signals an error occurred during the background process.
Here’s an example using the -n option:
#!/bin/bash
sleep 3 &
sleep 30 &
sleep 5 &
wait -n
echo "First job completed."
wait
echo "All jobs completed."
When this script runs, it spawns three background processes. The wait -n line blocks until the first job finishes (sleep 3 in this case), then prints the first message. The second wait without arguments blocks until all remaining background jobs complete.
First job completed. All jobs completed.
The -f option example demonstrates the difference between job status changes and actual termination. Open your terminal and run:
sleep 3600 &
pid=$!
echo "PID: $pid"
wait "$pid"
In another terminal, stop the process (but don’t kill it) using the PID printed above:
kill -STOP 46671
Use the PID printed in the first terminal; replace 46671 with that value.
Without the -f flag, wait returns immediately when the process status changes to “stopped.” The job hasn’t terminated, but wait considers the status change sufficient to return.
Now repeat the test with wait -f:
sleep 3600 &
pid=$!
echo "PID: $pid"
wait -f "$pid"
Send the stop signal from another terminal:
kill -STOP 46671
Again, substitute your actual PID for 46671.
This time wait doesn’t return when the process stops. It continues blocking until the process actually terminates (either from kill -TERM or kill -KILL).
Advanced Usage of the Wait Command
Beyond basic synchronization, wait enables sophisticated parallel processing patterns. The following examples show real-world scenarios where wait manages multiple concurrent tasks effectively.
Bash Wait for Multiple Processes
When running several independent tasks that all need to finish before proceeding, wait can synchronize them. This pattern appears frequently in backup scripts, batch processing, and deployment automation:
#!/bin/bash
echo "Starting process 1..."
sleep 10 &
pid1=$!
echo "Starting process 2..."
sleep 15 &
pid2=$!
echo "Starting process 3..."
sleep 20 &
pid3=$!
echo "Waiting for processes to complete..."
wait $pid1 $pid2 $pid3
echo "All processes completed."
This script launches three background jobs and stores each PID. The single wait command with all three PIDs blocks until every process finishes. Only then does execution continue to the final echo statement. This guarantees all parallel work completes before the script moves forward.
Using Bash Wait with a Loop
Loops combine well with wait when you’re processing multiple items in parallel. This approach works for batch file conversions, parallel downloads, or processing log files across multiple servers:
#!/bin/bash
for i in {1..5}
do
echo "Starting process $i..."
sleep $i &
pids[${i}]=$!
done
echo "Waiting for processes to complete..."
for pid in ${pids[*]}; do
wait $pid
done
echo "All processes completed."
The first loop spawns five background jobs and stores their PIDs in an array. The second loop iterates through the array, calling wait on each PID individually. Each wait blocks until that specific process completes. This pattern gives you finer control than waiting for all PIDs at once, allowing you to add logging, error handling, or progress tracking for each job.
Real-World Example: Parallel File Processing
Here’s a practical scenario showing how wait coordinates parallel file compression tasks:
#!/bin/bash
# Compress multiple log files in parallel
for logfile in /var/log/*.log; do
gzip -c "$logfile" > "$logfile.gz" &
pids+=($!)
done
echo "Compressing ${#pids[@]} files..."
for pid in ${pids[@]}; do
if ! wait "$pid"; then
echo "Warning: Process $pid failed"
fi
done
echo "Compression complete."
This script compresses all log files simultaneously instead of one at a time. The loop waits on each PID individually, so every job is reaped exactly once and any failure surfaces in the warning message. On a multi-core system, this parallel approach can reduce total processing time compared to sequential compression, depending on CPU and disk throughput.
CPU-Aware Parallel Processing
When processing many files, launching unlimited background jobs can overwhelm your system. This example limits parallel jobs to match available CPU cores, ensuring efficient resource usage without thrashing:
#!/bin/bash
# Get the number of CPU cores
max_jobs=$(nproc)
running=0
echo "Processing with up to $max_jobs parallel jobs..."
for video in /home/user/videos/*.mp4; do
# Wait for a slot if we're at capacity
if ((running >= max_jobs)); then
wait -n
((running--))
fi
# Start the conversion in background
ffmpeg -i "$video" -c:v libx265 "${video%.mp4}_compressed.mp4" &
((running++))
done
# Wait for remaining jobs
wait
echo "All conversions complete."
The nproc command returns the number of available processing units, letting the script adapt to different systems automatically. Before launching each background job, the script checks whether it’s already running the maximum number of parallel tasks. When the limit is reached, wait -n pauses until any single job finishes, freeing a slot for the next task. This pattern maintains optimal CPU utilization without spawning hundreds of processes that would compete for resources and slow everything down.
The running counter tracks how many jobs are active. Each time wait -n returns (indicating one job finished), the counter decrements to reflect the newly available slot. After the loop processes all files, a final wait ensures the last batch of jobs completes before the script exits. This approach works equally well for video encoding, image processing, log compression, or any task where files process independently.
Track Completed Job IDs with wait -p
Bash 5.1 introduced the -p flag to store the PID that satisfied wait -n into a variable. This makes it easy to report which remote restart or build job just finished:
#!/bin/bash
hosts=(web1 web2 web3)
for host in "${hosts[@]}"; do
ssh "$host" "sudo systemctl restart myapp" &
done
while true; do
if wait -n -p finished_pid; then
echo "Restart completed for PID $finished_pid"
else
echo "No more running jobs."
break
fi
done
Each iteration waits for whichever background restart finishes first, prints its PID, and loops again until wait returns 127 to signal that no jobs remain.
Restart completed for PID 24561 Restart completed for PID 24582 Restart completed for PID 24596 No more running jobs.
Troubleshoot Common wait Command Errors
The following issues commonly arise when using wait in shell scripts. Each entry shows the error, explains the cause, and provides a fix.
wait Returns “not a child of this shell”
Attempting to wait on a PID that wasn’t spawned by the current shell produces this error:
bash: wait: pid 12345 is not a child of this shell
The wait command only tracks child processes of the current shell session. Processes started by other users, other terminals, or parent scripts are invisible to wait.
To monitor a non-child process, poll its PID with kill -0 instead:
while kill -0 "$pid" 2>/dev/null; do
sleep 1
done
echo "Process $pid has exited."
wait -n Returns “invalid option”
Running wait -n on an older Bash version produces:
bash: wait: -n: invalid option
The -n flag requires Bash 4.3 or newer, and -p requires Bash 5.1. Check your version and upgrade if needed:
bash --version | head -n 1
GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)
Subshells and Pipelines Lose Track of PIDs
Background processes launched inside subshells or pipelines create a separate process tree. The parent shell cannot wait for them because they are not direct children:
# This PID is invisible to the parent shell's wait
(long_running_task &)
wait $! # Fails: PID belongs to the subshell
Launch background jobs directly in the current shell instead of wrapping them in subshells:
# Correct: launch in the current shell
long_running_task &
wait $!
Exit Status Returns from the Wrong Process
When passing multiple PIDs to a single wait call, it returns the exit status of the last PID listed, not necessarily the one that failed:
# Only captures exit status of pid3
wait "$pid1" "$pid2" "$pid3"
echo $? # Exit status of pid3 only
To catch every failure, check each PID individually in a loop:
failed=0
for pid in "${pids[@]}"; do
if ! wait "$pid"; then
echo "Process $pid failed with status $?"
failed=1
fi
done
exit $failed
Debugging wait with jobs and ps
When wait doesn’t behave as expected, these diagnostic commands help identify the problem:
# List all background jobs with PIDs and status
jobs -l
# Show all child processes of the current shell
ps --ppid $$
# Check if a specific PID still exists
kill -0 "$pid" 2>/dev/null && echo "Running" || echo "Gone"
You can also pipe ps output through grep to filter for specific process names. A sample jobs -l snapshot shows job numbers, PIDs, and current state:
[1] 35642 Running gzip -c access.log > access.log.gz [2]- 35801 Running ffmpeg -i input.mp4 output.mp4 [3]+ 35977 Done rsync -a /srv/files/ user@nas:/data/
If a process doesn’t appear in jobs -l, it either finished already or was spawned outside the current shell, both of which prevent wait from tracking it.
When Not to Use wait
The wait command fits specific use cases well, but several scenarios call for different tools. Choosing the right approach depends on how many parallel jobs you need, how granular your error handling must be, and whether processes span multiple scripts or users.
| Scenario | Use Instead | Why |
|---|---|---|
| Running dozens of parallel tasks with throttling | GNU Parallel or xargs -P | Built-in job limiting, progress tracking, and retry logic |
| Managing long-running services | systemd or supervisord | Process supervision with restart policies, logging, and dependency ordering |
| Sequential scripts with no background jobs | No synchronization needed | Adding wait to purely sequential code introduces unnecessary complexity |
| Coordinating across separate scripts or users | File locks (flock) or IPC mechanisms | wait only tracks child processes of the current shell |
Frequently Asked Questions About the wait Command
The wait command pauses until a background process finishes and returns its exit status. The sleep command pauses for a fixed number of seconds regardless of other processes. Use wait to synchronize with background jobs, and sleep when you need a timed delay.
No. The wait command only operates on child processes of the current shell. Attempting to wait on a PID from another session returns the error “not a child of this shell” with exit status 127. To monitor external processes, poll with kill -0 in a loop instead.
The basic wait command (with no flags) works in sh, bash, and zsh. However, the -n flag requires Bash 4.3 or newer, and -p requires Bash 5.1. Zsh supports wait -n natively but does not support -p. POSIX sh only supports the basic wait syntax without any flags.
The wait command has no built-in timeout option. To implement a timeout, run a background sleep as a deadline and use wait -n to return when either the target process or the timer finishes first. Check which PID completed using the -p flag (Bash 5.1+) to determine whether the process finished or the timeout expired.
Conclusion
The bash wait command gives you direct control over background process synchronization. Tracking PIDs in arrays, checking exit statuses with loops, throttling parallel jobs with wait -n, and identifying completed processes with -p cover the majority of real-world parallel scripting needs. For workloads beyond what wait handles, GNU Parallel and xargs -P provide additional job management capabilities.
Formatting tips for your comment
You can use basic HTML to format your comment. Useful tags currently allowed:
<code>command</code>command<strong>bold</strong><em>italic</em><blockquote>quote</blockquote>