ResilienceKit

0.5.1

AltiAntonov/ResilienceKit

What's New

ResilienceKit 0.5.1

2026-08-06T09:21:43Z

0.5.1

  • Normalized non-finite exponential backoff multipliers to behave as 1
  • Documented modifier precedence for policies, attempts, delay strategies, and retry predicates
  • Clarified that maxDelay is applied before jitter
  • Added package manifest and DocC validation to CI
  • Added policy runtime and edge-case tests

ResilienceKit

Small async retry primitives for Swift with explicit attempts and terminal cancellation.

Swift version compatibility Platform compatibility MIT License Swift workflow

Features · Installation · Quick Start · When To Use · Good Fits · Weaker Fits · Runtime Semantics · Documentation · Testing

Features

  • async-first retry entry point with Retry { ... }
  • explicit .maxAttempts(_:) configuration
  • fixed delay support through .delay(_:)
  • exponential backoff and bounded jitter
  • policy presets through .policy(_:)
  • retry predicates through .retry { ... }
  • terminal cancellation that is not retried
  • small surface area intended to grow in layers

The current public API is intentionally centered on:

  • Retry
  • RetryPolicy
  • RetryJitter

Installation

Add ResilienceKit to your Swift Package Manager dependencies:

dependencies: [
    .package(url: "https://github.com/AltiAntonov/ResilienceKit.git", from: "0.5.1")
]

Then add the product to your target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "ResilienceKit", package: "ResilienceKit")
    ]
)

Quick Start

import ResilienceKit

let value = try await Retry {
    try await fetchProfile()
}
.policy(.exponentialBackoff(maxAttempts: 4))
.retry { error in
    error is URLError
}
.run()

Policy presets are the shortest path for common setups:

try await Retry {
    try await refreshToken()
}
.policy(.immediate(maxAttempts: 3))
.run()
try await Retry {
    try await fetchProfile()
}
.policy(.fixedDelay(.seconds(1), maxAttempts: 3))
.run()
try await Retry {
    try await fetchProfile()
}
.policy(.exponentialBackoff(
    maxAttempts: 4,
    baseDelay: .milliseconds(250),
    multiplier: 2,
    maxDelay: .seconds(5),
    jitter: .fraction(0.2)
))
.run()

maxAttempts always means total executions, including the first call.

When To Use

Use ResilienceKit when you want retry behavior to be explicit, readable, and reusable instead of re-implementing ad hoc retry loops around async work.

It is a strong fit when the first thing you need is a small retry primitive, not a full resilience framework.

Good Fits

  • app and SDK code that wraps async network requests
  • codebases that want one obvious retry call site instead of repeated for loops
  • teams that want retry, fixed delay, backoff, and jitter behind one call-site shape
  • teams that want named presets for common retry behavior
  • code that needs to retry transient errors while failing fast on permanent errors
  • small packages or apps that want focused retry behavior without unrelated dependencies

Weaker Fits

  • projects that need HTTP-specific retry helpers today
  • systems that already require a broader resilience stack such as circuit breaking or rate limiting
  • sync-only code paths
  • packages that need broad platform coverage below iOS 17 or macOS 14 right now

Runtime Semantics

  • .maxAttempts(_:) controls the total number of attempts, not retries-after-the-first
  • values below 1 are clamped to 1
  • .policy(_:) applies a named preset for attempts and delay behavior
  • .policy(_:) sets both attempts and delay strategy; the last setter for each dimension wins
  • .retry(_:) remains independent from policy and delay modifiers
  • RetryPolicy.immediate(maxAttempts:) retries with no waiting between attempts
  • RetryPolicy.fixedDelay(_:maxAttempts:) retries with a fixed delay between attempts
  • RetryPolicy.exponentialBackoff(maxAttempts:baseDelay:multiplier:maxDelay:jitter:) retries with growing delay
  • .delay(_:) configures a fixed delay between failed attempts and defaults to .zero
  • .exponentialBackoff(baseDelay:multiplier:maxDelay:jitter:) configures growing retry delays
  • exponential multipliers below 1 and non-finite multipliers behave as 1
  • maxDelay is applied before jitter, so jitter can produce a final delay above maxDelay
  • RetryJitter.fraction(_:) applies bounded randomness to reduce synchronized retries
  • .retry { ... } decides whether a non-cancellation error is retryable
  • the first attempt always starts immediately
  • delay is applied only between eligible retries, never after the final failed attempt
  • non-retryable errors are thrown immediately without delay or another attempt
  • cancellation before the first attempt prevents the operation from running
  • CancellationError is terminal and is rethrown without additional attempts
  • cancellation during delay is rethrown and no later attempt runs
  • all non-cancellation thrown errors are retried until attempts are exhausted

HTTP-specific retry helpers are intentionally deferred to later releases.

Documentation

The package now includes a DocC catalog in Sources/ResilienceKit/ResilienceKit.docc.

Once Swift Package Index processes .spi.yml, hosted documentation should appear on the package page automatically.

Testing

The package uses Swift Testing.

Current coverage verifies:

  • first-attempt success
  • retry-until-success behavior
  • fixed delay between failed attempts
  • exponential backoff progression
  • policy preset behavior
  • bounded jitter behavior
  • selective retry behavior
  • fail-fast behavior for non-retryable errors
  • no trailing delay after the final failed attempt
  • exact call count on persistent failure
  • clamp behavior for invalid attempt counts
  • terminal cancellation from both pre-cancelled tasks, thrown CancellationError, and cancellation during delay

Description

  • Swift Tools 6.0.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Aug 30 2026 20:51:03 GMT-0900 (Hawaii-Aleutian Daylight Time)