Skip to content

Instantly share code, notes, and snippets.

@brandonhimpfen
Created February 15, 2026 21:05
Show Gist options
  • Select an option

  • Save brandonhimpfen/eafb4219ff5b4ace389b43eda268877c to your computer and use it in GitHub Desktop.

Select an option

Save brandonhimpfen/eafb4219ff5b4ace389b43eda268877c to your computer and use it in GitHub Desktop.
Bash retry helper with exponential backoff + random jitter (useful for distributed systems and flaky network calls).
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Bash retry helper with exponential backoff + jitter
# -----------------------------------------------------------------------------
# Why jitter?
# - If many clients retry at the same fixed interval, they can synchronize and
# cause thundering herds. Jitter spreads retries out randomly.
#
# Usage:
# retry_jitter <attempts> <base_delay_seconds> <max_delay_seconds> <command...>
#
# Examples:
# retry_jitter 6 1 30 curl -f https://example.com
# retry_jitter 8 0.5 10 bash -c 'echo hello && exit 1'
#
# Notes:
# - Works in strict mode.
# - Jitter is FULL jitter: sleep random(0, backoff_delay) each retry.
# - Supports fractional seconds (uses awk for math + randomness).
# -----------------------------------------------------------------------------
set -Eeuo pipefail
usage() {
cat <<EOF >&2
Usage: $(basename "$0") <attempts> <base_delay_seconds> <max_delay_seconds> <command...>
Example:
$(basename "$0") 6 1 30 curl -f https://example.com
EOF
}
# random_sleep(max_seconds): sleeps for a random duration in [0, max_seconds]
# Supports fractional seconds.
random_sleep() {
local max="$1"
awk -v max="$max" 'BEGIN {
# If max <= 0, sleep 0
if (max <= 0) { printf "0\n"; exit }
srand()
printf "%.3f\n", rand() * max
}' | {
read -r s
sleep "$s"
}
}
# min(a, b) for numbers (supports decimals)
min_num() {
local a="$1" b="$2"
awk -v a="$a" -v b="$b" 'BEGIN { print (a < b) ? a : b }'
}
retry_jitter() {
local attempts="${1:-}"
local base_delay="${2:-}"
local max_delay="${3:-}"
shift 3 || true
if [[ -z "$attempts" || -z "$base_delay" || -z "$max_delay" || $# -eq 0 ]]; then
usage
return 2
fi
local count=1
local exit_code=0
local backoff="$base_delay"
while (( count <= attempts )); do
echo "Attempt $count/$attempts: $*"
if "$@"; then
return 0
fi
exit_code=$?
if (( count == attempts )); then
echo "Command failed after $attempts attempts." >&2
return "$exit_code"
fi
# Cap backoff to max_delay
backoff="$(min_num "$backoff" "$max_delay")"
echo "Command failed (exit $exit_code). Backoff cap=${backoff}s, sleeping with jitter..." >&2
# FULL jitter: sleep random(0, backoff)
random_sleep "$backoff"
# Exponential backoff: backoff *= 2
backoff="$(awk -v b="$backoff" 'BEGIN { printf "%.3f\n", b * 2 }')"
((count++))
done
return "$exit_code"
}
# ---------------------------------------------------------------------------
# CLI entry (optional)
# ---------------------------------------------------------------------------
main() {
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" || $# -lt 4 ]]; then
usage
exit 0
fi
retry_jitter "$@"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment