As a sysadmin, you’ll often background tasks with & to fetch data, rotate logs, warm caches, or run health checks in parallel. That buys you concurrency, but unless you coordinate when to continue, you’ll invite race conditions and brittle runs.

That’s where wait shines. It blocks the current shell until a specific background process—or all of them—finishes. Used well, it turns ad-hoc parallelism into predictable orchestration.

Below is a practical, sysadmin-oriented guide: single and multiple waits, -n patterns, exit-code handling, common pitfalls (PIDs, pipelines, job control), and a simple concurrency limiter you can drop into any script.


Prerequisites

  • Linux/Unix environment with Bash (4.3+ for wait -n).
  • Basic shell scripting.
  • Ability to create and execute .sh files.

Syntax & core variables

wait [PID...]
Code language: CSS (css)
  • wait (no args): wait for all background jobs started by the shell.
  • wait PID: wait for that PID only.
  • $!: PID of the last backgrounded command.
  • $?: after wait, the exit status of the process just waited for.
  • wait -n: return as soon as any one running background job finishes.

Notes

  • Waiting on a non-existent PID returns 127.
  • With multiple PIDs, wait PID1 PID2 ... waits for all and returns the status of the last one reaped.

Case 1 — One background job, wait for all

#!/bin/bash
echo "Starting background process..."
sleep 5 &
echo "Waiting..."
wait
echo "Done."
Code language: PHP (php)
  • sleep 5 & goes to the background.
  • Bare wait blocks until no background jobs remain.

Case 2 — Wait for a specific PID

#!/bin/bash
sleep 3 & pid=$!
echo "Waiting for PID $pid..."
wait "$pid"
echo "PID $pid ended with status $?"
Code language: PHP (php)
  • Capture $! immediately after &.
  • wait "$pid" affects only that process.

Case 3 — Two jobs, explicit order

#!/bin/bash
echo "Starting two tasks..."
sleep 3 & pid1=$!
sleep 2 & pid2=$!

wait "$pid1"; echo "Job1 (PID $pid1) status: $?"
wait "$pid2"; echo "Job2 (PID $pid2) status: $?"
Code language: PHP (php)
  • Useful when one result gates the next steps.

Case 4 — Wait for everyone, no PIDs

#!/bin/bash
sleep 2 &  sleep 4 &
wait
echo "All background jobs finished."
Code language: PHP (php)
  • Perfect when order is irrelevant, only emptiness matters.

Case 5 — wait -n: react when any job completes

Return as soon as one job finishes; great for streaming results or replenishing a worker pool.

#!/bin/bash
sleep 3 & sleep 5 &
wait -n
echo "At least one job has finished."
Code language: PHP (php)

Consume completions in a loop:

#!/bin/bash
sleep 1 & p1=$!
sleep 4 & p2=$!
sleep 2 & p3=$!

# While there are running jobs...
while jobs -rp >/dev/null; do
  if wait -n; then
    echo "A job completed successfully."
  else
    echo "A job failed with status $?"
  fi
done
echo "No jobs left."
Code language: PHP (php)
  • jobs -rp prints running job PIDs (quiet test).
  • Each wait -n returns the exit status of whichever job just ended.

Mapping exit codes to commands (who failed?)

Keep PIDs and labels side-by-side:

#!/bin/bash
declare -a PIDS NAMES

run() { "$@" & PIDS+=($!); NAMES+=("$*"); }

run curl -fsS https://example.org/a
run curl -fsS https://example.org/b
run curl -fsS https://example.org/c

for i in "${!PIDS[@]}"; do
  pid=${PIDS[$i]} name=${NAMES[$i]}
  if wait "$pid"; then
    echo "[OK] $name (PID $pid)"
  else
    rc=$?
    echo "[FAIL] $name (PID $pid) exit=$rc"
  fi
done
Code language: PHP (php)
  • wait "$pid" succeeds if the process returned 0, otherwise returns the process’s exit code.

Waiting by job spec (%1, %2)—useful interactively

#!/bin/bash
sleep 3 & sleep 5 &
jobs
wait %1
echo "%1 done"
wait
echo "All done"
Code language: PHP (php)

In non-interactive scripts, PID-based control is more robust than %n job specs.


Pipelines, $!, and pipefail (sharp edges)

  • In cmd1 | cmd2 &, $! is the last element of the pipeline (often cmd2) or the subshell hosting it, depending on grouping. If you care about the whole pipeline, wrap it: { cmd1 | cmd2; } & pid=$! wait "$pid"
  • If the pipeline’s overall success matters, enable: set -o pipefail This makes the pipeline’s status reflect the rightmost non-zero exit code—vital for failing fast in automation.

Timeouts with wait (there isn’t one—compose it)

wait has no native timeout. Use timeout(1):

sleep 60 & pid=$!
if timeout 10s bash -c "wait $pid"; then
  echo "Finished within 10s"
else
  echo "Timed out; killing $pid"
  kill -TERM "$pid" 2>/dev/null
  wait "$pid" 2>/dev/null
fi
Code language: JavaScript (javascript)

Or roll your own watcher with kill -0 in a loop and sleep 1.


Lightweight concurrency limiter (no GNU Parallel required)

Keep N jobs running; start new ones as others complete:

#!/bin/bash
set -euo pipefail
max=4
pids=()

launch() {
  { "$@" & } ; pids+=($!)
}

prune() {
  local alive=()
  for p in "${pids[@]}"; do
    kill -0 "$p" 2>/dev/null && alive+=("$p")
  done
  pids=("${alive[@]}")
}

for f in *.tar.gz; do
  launch tar -tzf "$f" >/dev/null

  if (( ${#pids[@]} >= max )); then
    wait -n || echo "A job failed with $?"
    prune
  fi
done

wait || true   # reap remaining; don’t abort on a single failure here
echo "All verifications completed."
Code language: PHP (php)
  • wait -n lets the pool refill as soon as any job ends.
  • kill -0 probes liveness without sending a signal.

Common mistakes (and how to avoid them)

  1. Capturing the wrong PID
    Always read $! immediately after &. Another backgroundable call in between will overwrite it.
  2. Confusing sleep and wait
    sleep delays by time; wait synchronizes on process completion.
  3. Relying on job control in scripts
    Job control may be disabled in non-interactive shells. Prefer PIDs ($!, jobs -rp) over %1/%2.
  4. Forgetting to save $?
    wait writes to $?. If you need that status later, stash it in a variable immediately.
  5. Ignoring pipeline semantics
    Without set -o pipefail, a failing left-hand command can be masked by a successful right-hand command.

Good practices for production scripts

  • Capture PIDs immediately and store them with labels.
  • Check exit codes and log failures clearly (PID, command, status).
  • Use wait -n for reactive flows (first-ready handling).
  • Prefer PID waits to job specs in automation.
  • Document your concurrency model in the script header.

Conclusion

wait is tiny but pivotal: it’s the difference between “seems to work” and reliable parallel automation. Use it to:

  • Eliminate race conditions.
  • Synchronize stages of multi-step jobs.
  • Keep scripts deterministic even with dozens of workers.

Combine wait with $! and -n, capture exit codes, and layer timeouts or pool limits as needed. Your future self—and your on-call rotation—will thank you.


FAQ

What does wait return?
The exit status of the process it reaped (0 on success). If the PID doesn’t exist, it returns 127. With -n, it returns the status of whichever job finished next.

Can I wait on grandchildren?
No—wait only waits for processes started by the current shell. If a child forks further, wait on the top-level child you launched or add supervision.

Is wait -n always available?
It requires Bash 4.3+. On older systems, emulate it by polling jobs -rp and using targeted wait calls or by supervising with SIGCHLD/loops.

When should I reach for GNU Parallel / xargs -P instead?
For bulk data processing over big argument lists, those tools offer concise, high-throughput patterns with built-in concurrency limits. For fine-grained control inside a script (ordering, staged gates, custom error handling), wait + PIDs gives you the steering wheel.

Scroll to Top