Run Shell Commands in Parallel with Bash
Execute multiple shell commands concurrently using xargs, GNU parallel, and background jobs.
Overview
Running commands sequentially is slow when tasks are independent. Bash has three built-in approaches for parallelism: background jobs with &, xargs -P, and GNU parallel. The following demonstrates how to all three with practical examples for batch file processing, image conversion, and API calls.
When to Use
- You need to process hundreds of files (resize, convert, compress)
- You are making API calls to multiple endpoints
- You want to speed up batch operations that are I/O bound
- You need to run independent shell commands concurrently
Solution
Background jobs with wait
#!/bin/bash
process_file() {
local file="$1"
gzip "$file"
echo "Done: $file"
}
for file in *.log; do
process_file "$file" &
done
wait
echo "All files processed"
Limit concurrency with xargs
# Compress 4 files at a time
find . -name "*.log" -print0 | xargs -0 -P4 -I{} gzip {}
# Resize images 8 at a time
ls *.jpg | xargs -P8 -I{} convert {} -resize 50% small_{}
GNU parallel
# Install if needed
# apt install parallel
# Process files in parallel with progress bar
ls *.jpg | parallel -j 8 convert {} -resize 50% small_{}
# Process with progress and ETA
ls *.jpg | parallel --progress -j 8 convert {} -resize 50% small_{}
# Keep output ordered
ls *.txt | parallel -k grep "error" {}
Parallel with custom function
#!/bin/bash
process_url() {
local url="$1"
local output=$(curl -s -o /dev/null -w "%{http_code}" "$url")
echo "$url: $output"
}
export -f process_url
urls=(
"https://api.example.com/users"
"https://api.example.com/posts"
"https://api.example.com/comments"
)
printf '%s\n' "${urls[@]}" | parallel -j 4 process_url {}
Collect results from parallel jobs
#!/bin/bash
# Run commands in background and collect exit codes
declare -a pids
declare -a results
for i in 1 2 3 4 5; do
(sleep $((RANDOM % 5)); echo "Task $i done") &
pids+=($!)
done
for pid in "${pids[@]}"; do
wait "$pid"
results+=("PID $pid exited with $?")
done
for result in "${results[@]}"; do
echo "$result"
done
Parallel file downloads
#!/bin/bash
urls_file="downloads.txt"
# downloads.txt contains one URL per line
cat "$urls_file" | xargs -P4 -I{} wget -q {}
# Or with GNU parallel
cat "$urls_file" | parallel -j 4 wget -q {}
Parallel with output to separate files
# Each command writes to its own log file
ls *.txt | parallel 'grep "error" {} > {.}.errors'
# {.} removes the extension, so file.txt becomes file.errors
Explanation
Background jobs (&) send a process to the background and return immediately. wait blocks until all background jobs finish. This is the simplest approach but has no built-in concurrency limit. If you start 1000 jobs, you get 1000 simultaneous processes.
xargs -P N runs up to N commands in parallel. It reads items from stdin and passes each to the command. -0 handles filenames with spaces. -I{} lets you position the argument precisely.
GNU parallel is the most capable tool. It supports:
-j N: Limit concurrent jobs to N.-k: Keep output in order of input (not completion order).--progress: Show a progress bar.{}: Placeholder for the input item.{.}: Input item without extension.--eta: Show estimated time of completion.
Variants
| Approach | Concurrency Control | Ordering | Use When |
|---|---|---|---|
& + wait | None | None | Few jobs, simple scripts |
xargs -P | Fixed (-P N) | None | Batch file processing |
| GNU parallel | Fixed (-j N) | Optional (-k) | Complex parallel workflows |
coproc | Single | None | Bidirectional communication |
Guidelines
- Limit concurrency to the number of CPU cores for CPU-bound tasks. Use
-j $(nproc). - For I/O-bound tasks (downloads, API calls), higher concurrency (8-16) is fine.
- Use
xargs -0orparallelto handle filenames with spaces correctly. - Export functions with
export -fbefore using them inxargsorparallel. - Use
--dry-runwithparallelto preview commands before running them.
Common Mistakes
- Starting too many background jobs without a limit. This can exhaust memory or file descriptors.
- Not using
waitafter background jobs. The script exits before jobs finish. - Forgetting
-0withxargswhen filenames contain spaces. Files get split on spaces. - Not exporting functions when using them with
parallel. The function is not available in subshells. - Mixing output from parallel jobs without
-k. Output interleaves and becomes unreadable.
Performance Tips
- Benchmark different concurrency levels. The optimal
-jvalue depends on your workload:
# Test with different concurrency
for j in 1 2 4 8 16; do
time find . -name "*.log" | parallel -j $j gzip {}
done
- Use
--round-robinfor uneven workloads. Distributes work more evenly when jobs vary in size:
# Group files by size, then distribute
find . -name "*.log" -exec du -b {} + | sort -n | parallel --round-robin -j 4 gzip {2}
- Pin jobs to CPU cores with
taskset. For CPU-bound work, avoid context switching:
# Assign each parallel job to a specific core
parallel -j $(nproc) 'taskset -c %{} gzip {}' ::: *.log Frequently Asked Questions
How do I limit parallelism to the number of CPU cores?
find . -name "*.jpg" | parallel -j $(nproc) convert {} -resize 50% small_{}
How do I retry failed commands with GNU parallel?
Use --retries N:
cat urls.txt | parallel --retries 3 wget -q {}
Can I use parallel with SSH?
Yes. GNU parallel can run commands on remote machines:
parallel --sshlogin server1,server2 -j 2 --transfer --return {}.out --cleanup "process.sh {}" ::: file1 file2
How do I show a progress bar with xargs?
xargs does not have a built-in progress bar. Use parallel --progress instead, or pipe through pv:
cat urls.txt | pv -l | xargs -P4 -I{} wget -q {}
Semaphore Pattern for Controlled Concurrency
#!/bin/bash
# semaphore.sh — limit background jobs with a semaphore
MAX_JOBS=4
open_semaphores() {
for i in $(seq 1 $MAX_JOBS); do
echo
done
}
run_with_semaphore() {
read -r line <&3
(
"$@"
) 3>&1
}
# Open semaphore
exec 3< <(open_semaphores)
for file in *.log; do
run_with_semaphore process_file "$file" &
done
wait
echo "All done with max $MAX_JOBS concurrent jobs"
Error Handling in Parallel Jobs
#!/bin/bash
# parallel-with-errors.sh
process_with_error() {
local file="$1"
if gzip "$file" 2>/dev/null; then
echo "OK: $file"
else
echo "FAIL: $file" >&2
return 1
fi
}
export -f process_with_error
# Capture exit codes
find . -name "*.log" | parallel -j 4 process_with_error {}
EXIT_CODES=$?
if [ $EXIT_CODES -ne 0 ]; then
echo "Some jobs failed. Exit code: $EXIT_CODES"
exit 1
fi
Timeout per Job
#!/bin/bash
# timeout-parallel.sh
# Each job has a 30-second timeout
cat urls.txt | parallel -j 8 --timeout 30 wget -q {}
# With GNU timeout command for custom functions
process_with_timeout() {
local url="$1"
timeout 30 curl -s -o /dev/null -w "%{http_code}" "$url" || echo "TIMEOUT: $url"
}
export -f process_with_timeout
cat urls.txt | parallel -j 8 process_with_timeout {}
Parallel with Logging
#!/bin/bash
# parallel-logging.sh
LOGDIR="./logs"
mkdir -p "$LOGDIR"
process_and_log() {
local file="$1"
local logfile="$LOGDIR/$(basename "$file").log"
{
echo "Start: $(date -Iseconds)"
gzip "$file"
echo "End: $(date -Iseconds) exit=$?"
} > "$logfile" 2>&1
}
export -f process_and_log
find . -name "*.log" -not -path "./logs/*" | parallel -j 4 process_and_log {}
Related Resources
Parallel Job Execution with Bash
Run shell commands and scripts in parallel safely using xargs, parallel, or background jobs with concurrency control.
RecipeBash Parallel Execution
How to run shell commands in parallel with xargs, GNU parallel, and Bash background jobs while controlling concurrency and collecting results.
RecipeBackup Rotation Script in Bash
Automated backup with retention policies using bash and find.
RecipeBash Scripting for DevOps Automation and System Tasks
How to write reliable Bash scripts for automating deployments, system monitoring, log rotation, and routine maintenance tasks
RecipeBash Loop Over Files
How to safely loop over files and directories in Bash, handling spaces, globs, and large file lists with correct patterns.
RecipeAWS CLI Bash Scripts
Automate AWS resource provisioning with bash and AWS CLI