Chapter 12Lesson 02~100 minutes

Retries, Backoff, and Transient Failure Handling

Retries are useful only when a failure may be temporary and the operation is safe to repeat. Reliable retry logic is bounded, delayed, classified, observable, and aware of ambiguous side effects.

IntermediateProduction reliabilityHands-on lab

Learning objectives

By the end of this lesson

  • Classify transient versus permanent failures.
  • Implement bounded retries.
  • Use exponential backoff and jitter.
  • Apply overall time budgets.
  • Avoid unsafe repetition of side effects.

1. Retry only failures that may actually be transient

Failure classExamplesPolicy
TransientTimeout, temporary network failure, 503Retry may help
PermanentInvalid credentials, syntax error, 404 policy failureRetry usually wastes time
UnknownAmbiguous remote outcomeReconcile state before repeating side effects

The first question is not “how many retries?” It is “is this failure safe and meaningful to retry?”

2. Every retry loop needs an attempt bound

max_attempts=4

for ((attempt=1; attempt<=max_attempts; attempt++)); do
  if perform_operation; then
    break
  fi

  if (( attempt == max_attempts )); then
    printf 'operation failed after %d attempts\n' "$attempt" >&2
    exit 1
  fi
done
Never retry forever

Infinite retry loops can turn a dependency incident into a permanently stuck deployment or CI runner.

3. Backoff reduces pressure on a failing dependency

delay=1
max_attempts=5

for ((attempt=1; attempt<=max_attempts; attempt++)); do
  if probe_endpoint; then
    exit 0
  fi

  (( attempt == max_attempts )) && break
  sleep "$delay"
  (( delay *= 2 ))
done

exit 1

Exponential backoff gives a struggling service progressively more recovery time.

4. Jitter prevents synchronized retry storms

base_delay=$(( 2 ** (attempt - 1) ))
jitter=$(( RANDOM % 1000 ))
sleep_seconds=$(awk -v b="$base_delay" -v j="$jitter" \
  'BEGIN { printf "%.3f", b + (j / 1000) }')

sleep "$sleep_seconds"

When many clients fail at once, identical retry schedules can create a thundering herd. Random jitter spreads the retries over time.

5. Retry policy should inspect the failure class

if curl --fail --silent --show-error --max-time 20 "$url"; then
  return 0
else
  status=$?
  case $status in
    6|7|28) return 75 ;;  # example transient classes
    *)      return "$status" ;;
  esac
fi
Tool-specific statuses

The exact retryable exit codes depend on the command. Read the tool's documented status contract rather than guessing.

6. HTTP status and transport status are different layers

A request can fail before HTTP exists, or it can receive an HTTP response that indicates temporary or permanent failure. Production retry logic often needs both transport-level and application-level classification.

7. Use an overall time budget, not only attempt count

deadline=$((SECONDS + 60))

while (( SECONDS < deadline )); do
  if perform_operation; then
    exit 0
  fi
  sleep 2
done

printf 'retry budget exhausted\n' >&2
exit 1

Attempt count alone can be misleading when individual attempts have variable duration.

8. Retry safety depends on side effects

Reads and convergent updates are usually easier to retry. Creates, payments, message sends, deployments, and mutations may require an idempotency key, transaction identifier, or reconciliation step before another attempt.

Unknown outcome

If the client times out after sending a mutating request, the server may already have completed it. Reconcile remote state before blindly repeating.

9. Respect server-provided retry guidance

Some APIs provide a retry-after value or rate-limit reset time. Prefer server guidance over a locally invented aggressive retry schedule when the protocol defines it.

10. Log attempts without flooding output

printf 'retry attempt=%d/%d delay=%ss reason=%s\n' \
  "$attempt" "$max_attempts" "$delay" "$reason" >&2

Operators need to know that a command is retrying, why, and when it will stop.

11. Hands-on lab: reusable retry helper

mkdir -p "$HOME/devops-academy/bash/chapter12/lesson02"
cd "$HOME/devops-academy/bash/chapter12/lesson02"

retry() {
  local max_attempts=$1
  shift

  local attempt delay status
  delay=1

  for ((attempt=1; attempt<=max_attempts; attempt++)); do
    if "$@"; then
      return 0
    else
      status=$?
    fi

    if (( attempt == max_attempts )); then
      printf 'failed after %d attempts status=%d\n' \
        "$attempt" "$status" >&2
      return "$status"
    fi

    printf 'attempt=%d failed status=%d retry_in=%ss\n' \
      "$attempt" "$status" "$delay" >&2

    sleep "$delay"
    (( delay *= 2 ))
  done
}

attempt_file=./attempt
printf '0\n' > "$attempt_file"

flaky_demo() {
  n=$(cat "$attempt_file")
  ((n += 1))
  printf '%d\n' "$n" > "$attempt_file"
  printf 'demo attempt=%d\n' "$n"

  (( n >= 3 ))
}

retry 5 flaky_demo

Verification checklist

12. Knowledge check

Question 1. Which failures should generally be retried?

Question 2. Why add backoff?

Question 3. Why add jitter?

Question 4. What should happen after an ambiguous mutating timeout?

13. Summary

Reliable retries are classified, bounded, delayed, and observable. Use backoff and jitter, distinguish permanent from transient failure, respect remote guidance, and never retry side effects until you know repetition is safe.

14. Further reading

  • curl documentation — exit codes, retry, timeout behavior.
  • HTTP semantics — retryable methods and status codes.
  • AWS Architecture guidance — exponential backoff and jitter.
  • Google SRE guidance — handling overload and retries.
Next lesson

Structured Logging, Timestamps, and Verbosity Levels

Continue Chapter 12 by turning error-safe scripts into production-grade, recoverable automation.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.