Comprehensive guide to parallel execution in Bash: background processes, wait, jobs, xargs -P, GNU parallel, concurrency management, race conditions, and file locking with flock in DevOps.
In the previous post, we explored collecting system metrics and sending automated alerts. But there’s a common problem with monitoring scripts: when you need to check 50 servers simultaneously, sequential execution can take minutes—or even hours if each server takes a few seconds to respond.
Parallel Execution solves this: instead of waiting for server A to finish before moving to server B, you run both at the same time. Bash provides multiple mechanisms for parallelism—from simple &/wait, to convenient xargs -P, to powerful GNU parallel. This article covers leveraging parallelism to optimize DevOps script performance.
Basic Parallel Mechanisms in Bash
Background Processes with &
The & symbol at the end of a command runs it in the background, allowing Bash to continue executing the next statement:
# List background jobsjobs -l
# Count only running jobsjobs -r | wc -l
# Count only stopped jobsjobs -s | wc -l
PID Management
Each background process has a unique PID. You can save the PID for control:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Save PIDsleep 60&PID=$!echo"Started background job with PID: $PID"# Check if job is still runningifkill -0 "$PID" 2>/dev/null;thenecho"Job $PID is still running"elseecho"Job $PID has finished"fi# Stop the jobkill"$PID"# Force stopkill -9 "$PID"
Limiting Concurrency
Running too many background processes simultaneously can cause overload. The most common way to limit concurrency is using PID arrays or job counting:
MAX_PARALLEL=3pids=()for i in $(seq 1 10);doecho"Starting task $i..." sleep 2&# Simulate taskpids+=($!)# Wait when limit reachedif[[${#pids[@]} -ge $MAX_PARALLEL]];thenwait"${pids[0]}"pids=("${pids[@]:1}")# Remove first PIDfidone# Wait for remainingfor pid in "${pids[@]}";dowait"$pid"doneecho"All tasks completed."
Method 2: Job Counting
1
2
3
4
5
6
7
8
9
10
11
12
13
MAX_PARALLEL=3for server in server1 server2 server3 server4 server5;do ssh "$server""uptime"&# Wait when too many jobs runningwhile[[$(jobs -r | wc -l) -ge $MAX_PARALLEL]];do sleep 0.1
donedonewaitecho"All servers checked."
xargs — Parallel with -P
xargs is a powerful tool for running commands on input lists. The -P flag enables parallel execution:
# Run ssh on multiple serversparallel -j 3 ssh user@{}"df -h /" ::: server1 server2 server3 server4
# Run with multiple argumentsparallel -j 3echo"Processing {1} with {2}" ::: A B C ::: 123# Read input from fileparallel -j 5 < server_list.txt ssh {}"uptime"
Advanced Features
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Show progressparallel --progress -j 3 sleep {} ::: 12345# Show ETAparallel --eta -j 3 sleep {} ::: 12345# Results in orderparallel --keep-order -j 3echo{} ::: C B A
# Write output to separate filesparallel --results /tmp/results/ -j 3echo{} ::: task1 task2 task3
# Dry-run (print commands without executing)parallel --dry-run -j 3 ssh user@{}"apt update" ::: server1 server2 server3
#!/usr/bin/env bash
# parallel-ssh.sh — Run commands on multiple servers in parallel with GNU parallelset -euo pipefail
readonlySERVERS_FILE="${1:-servers.txt}"readonlyCOMMAND="${2:?Usage: $0 <servers.txt> <command>}"readonlyMAX_JOBS="${MAX_JOBS:-5}"readonlySSH_OPTS="-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"if[[ ! -f "$SERVERS_FILE"]];thenecho"Error: Server file $SERVERS_FILE not found" >&2exit1fiecho"Running on $(grep -cvE '^\s*#|^\s*$'"$SERVERS_FILE") servers with max $MAX_JOBS parallel jobs..."grep -vE '^\s*#|^\s*$'"$SERVERS_FILE"| parallel -j "$MAX_JOBS" --keep-order --joblog /tmp/parallel-ssh.log '
result=$(ssh '"$SSH_OPTS"' {} "'"${COMMAND}"'" 2>&1)
exit_code=$?
if [[ $exit_code -eq 0 ]]; then
echo "[OK] {}"
else
echo "[FAIL] {} (exit: $exit_code)"
fi
echo " Output: $result"
'echo""echo"Job log saved to /tmp/parallel-ssh.log"
Race Conditions and File Locking
When multiple processes write to the same file, race conditions can occur—data gets overwritten or interleaved. flock solves this with file locking:
The Race Condition Problem
1
2
3
4
5
6
# DANGEROUS: Multiple processes writing to same log → race conditionfor i in $(seq 1 10);doecho"Task $i completed" >> /tmp/shared.log &donewait# Result: May lose lines or be out of order
Solution with flock
1
2
3
4
5
6
7
8
9
# Safe: Use flock to lock file before writingfor i in $(seq 1 10);do( flock 200# Lock file descriptor 200echo"Task $i completed at $(date '+%H:%M:%S.%N')" >> /tmp/shared.log
) 200>/tmp/shared.log &donewait# Result: Correct order, no lost lines
Checking 8 servers with max 5 parallel jobs...
OK web-01
OK web-02
WARN db-01 disk:87% docker:down
OK web-03
OK worker-01
OK worker-02
FAIL bastion SSH connection failed
OK web-04
========================================
SUMMARY
Total: 8
OK: 6
WARN: 1
FAIL: 1
========================================
Failures:
FAIL bastion SSH connection failed
Deployment Notes & Best Practices
Always limit concurrency: Running too many SSH sessions or HTTP requests simultaneously can overload the network or source server. Always set MAX_JOBS appropriately—typically 5-10 for SSH, 20-50 for HTTP requests.
Race conditions when logging: When parallel scripts write to the same file, always use flock or have each process write to a separate file and aggregate afterward.
Test sequentially before parallelizing: Always run the script in sequential mode (MAX_JOBS=1) first to confirm logic is correct, then increase parallelism.
GNU parallel vs xargs:xargs -P is sufficient for most simple use cases. GNU parallel excels when you need: ordered output, progress/ETA display, complex error handling, or multi-host execution.
Timeouts: Always set timeouts for SSH and HTTP requests to prevent scripts from hanging indefinitely when a server is unresponsive.
Conclusion
Parallel Execution is an advanced but extremely useful skill in Bash scripting. From simple background processes with &/wait, to convenient xargs -P, to powerful GNU parallel—you can dramatically reduce processing time for large tasks. Combined with flock to prevent race conditions, you’ll have a safe and efficient parallel execution system.
In the next post, we will explore Bash and REST APIs: calling APIs with curl, parsing JSON with jq, sending bearer auth tokens, and automating workflows with GitHub/Slack APIs.