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.