Retries with exponential backoff

Networking Mid-level Swift 5.7+ iOS 16+ Reviewed By Alok Singh

Pick the fix

Which retry policy is best for a GET request?

Options
Hint 1 of 1

Which errors can succeed on a second try?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Retry timeouts, 5xx and 429 up to 3 times with exponential delay plus jitter, using Task.sleep(for:) so cancellation stops it

Correct. Transient errors only, bounded and cancellable.

Why not the others?
  • B Retry every error immediately, 10 times. It hammers the server and retries errors that can never succeed.
  • C Retry all errors, including 400 and 401, every second. Client errors won't fix themselves by retrying.
  • D Retry forever with a fixed 1-second delay. Unbounded retries waste battery and never surface the failure.

30-second answer

Retry only transient failures: timeouts, lost connections, 5xx responses and 429. Wait longer after each attempt (for example 0.5 s, 1 s, 2 s) plus random jitter, cap the number of attempts, and stop immediately if the task is cancelled. Never retry 4xx client errors, and only retry non-idempotent requests with an idempotency key.

Practise saying it · 45 s. No recording, just a timer

Explain your retry strategy.

Say your answer out loud, then check which of these points you covered:

  • Transient errors only
  • Exponential backoff + jitter
  • Cap and cancellation
  • Idempotency

Deep dive

try await Task.sleep(for: .seconds(delay)) throws on cancellation, which ends the loop.

Jitter stops many clients retrying in lockstep.

Honour a Retry-After header when the server sends one.

Surface the final error to the UI with a manual retry option.

Backoff helper Type-checked · Swift 6.4
func withRetry<T>(maxAttempts: Int = 3,
                  _ operation: () async throws -> T) async throws -> T {
    var attempt = 0
    while true {
        do {
            return try await operation()
        } catch let error where isTransient(error) && attempt < maxAttempts - 1 {
            attempt += 1
            let base = 0.5 * pow(2, Double(attempt - 1))
            try await Task.sleep(for: .seconds(base + .random(in: 0...0.3)))
        }
    }
}

Common mistakes

  • Retrying 400, 401 or 404.
  • Retrying instantly in a tight loop.

Senior take & follow-ups

Seniors classify errors first, then add backoff, jitter, caps and cancellation.

Follow-up questions

Think of your answer first, then open the hint.

  1. Why add jitter?

    Show hint

    To avoid a thundering herd after an outage.

  2. Should you retry a payment POST?

    Show hint

    Only with an idempotency key the server honours.

Suggest a fix if something here is wrong or out of date.