What you will learn
Run the same command across a list of hosts in parallel over SSH, collect per-host success/failure without one bad host blocking the rest, and produce a clean report at the end.
Before you begin
Requires key-based SSH access to every host already set up (no interactive password prompts). Test against a small inventory subset before a full fleet run.
1. Define the inventory and a safe per-host wrapper
#!/usr/bin/env bash
set -Eeuo pipefail
INVENTORY_FILE="hosts.txt" # one hostname per line
REMOTE_CMD="systemctl is-active myapp"
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new)
MAX_PARALLEL=8
run_on_host() {
local host="$1"
local out
if out="$(ssh "${SSH_OPTS[@]}" "$host" -- "$REMOTE_CMD" 2>&1)"; then
echo "OK ${host}: ${out}"
else
echo "FAIL ${host}: ${out}"
fi
}
BatchMode=yes makes SSH fail immediately instead of hanging on a password prompt when key auth isn't set up for a host — essential for an unattended fan-out, where one stuck prompt would otherwise stall the whole run.
2. Fan out with a concurrency cap
mapfile -t HOSTS < "$INVENTORY_FILE"
RESULTS_FILE="$(mktemp)"
running=0
for host in "${HOSTS[@]}"; do
[[ -z "$host" || "$host" == \#* ]] && continue # skip blanks/comments
run_on_host "$host" >> "$RESULTS_FILE" &
running=$((running + 1))
if (( running >= MAX_PARALLEL )); then
wait -n
running=$((running - 1))
fi
done
wait
3. Summarize without losing per-host detail
fail_count="$(grep -c '^FAIL' "$RESULTS_FILE" || true)"
total="$(wc -l < "$RESULTS_FILE")"
sort "$RESULTS_FILE"
echo "---"
echo "${total} hosts, ${fail_count} failed"
rm -f "$RESULTS_FILE"
(( fail_count == 0 ))
Appending each host's result to a shared file (rather than trying to aggregate return values from background jobs directly) sidesteps the fact that a backgrounded subshell's exit status is awkward to collect at scale — grep -c on the recorded output is simpler and more reliable.
Example output
FAIL db03.internal: connection timed out
OK db01.internal: active
OK db02.internal: active
OK db04.internal: active
---
4 hosts, 1 failed
Verify the result
Include one deliberately unreachable host in the inventory and confirm it's reported as FAIL with a clear reason while the rest still complete and report OK.
Troubleshooting
If every host fails with "Host key verification failed," StrictHostKeyChecking=accept-new still refuses a host whose key changed (as opposed to being unknown) — that's deliberate; investigate before overriding with no.
Next steps
Feed $REMOTE_CMD from getopts-cli-argument-parser so this becomes a general-purpose fleet executor, and route the failure summary through webhook-alert-dispatcher.