Introducing Typhoon - a lightweight async/await retry framework for Swift

Hello Swift community :waving_hand:

I’d like to introduce Typhoon, a small and focused Swift framework that provides robust retry policies for asynchronous operations, built natively on top of Swift’s async/await concurrency model.

The main idea behind Typhoon is simple:

retry logic is ubiquitous, but it shouldn’t be heavy, intrusive, or framework-specific.

Typhoon is intentionally minimal, type-safe, and dependency-free, while still being flexible enough to cover a wide range of real-world retry scenarios.

Motivation

In many Swift projects-especially those dealing with networking, databases, file systems, or third-party services-retry logic tends to be:

  • Reimplemented ad hoc

  • Scattered across the codebase

  • Tightly coupled to specific APIs or frameworks

  • Hard to test and reason about

With the introduction of structured concurrency in Swift, retrying async operations can be expressed much more clearly. Typhoon aims to provide a small, reusable abstraction that fits naturally into modern Swift code without imposing architectural constraints.

Retry Strategies

Typhoon currently provides three strategies:

case constant(retry: Int, duration: DispatchTimeInterval)

case exponential(
    retry: Int,
    multiplier: Double = 2.0,
    duration: DispatchTimeInterval
)

case exponentialWithJitter(
    retry: Int,
    jitterFactor: Double = 0.1,
    maxInterval: DispatchTimeInterval? = .seconds(60),
    multiplier: Double = 2.0,
    duration: DispatchTimeInterval
)

Constant Strategy

Best when predictable timing is required.

let service = RetryPolicyService(
    strategy: .constant(retry: 4, duration: .seconds(2))
)

Exponential Strategy

Useful for reducing pressure on unstable services.

let service = RetryPolicyService(
    strategy: .exponential(
        retry: 3,
        multiplier: 2.0,
        duration: .seconds(1)
    )
)

Exponential with Jitter

Designed to avoid thundering herd problems in distributed systems.

let service = RetryPolicyService(
    strategy: .exponentialWithJitter(
        retry: 5,
        jitterFactor: 0.2,
        maxInterval: .seconds(30),
        multiplier: 2.0,
        duration: .seconds(1)
    )
)

Documentation

The documentation for Typhoon is available directly in the repository and is intended to be practical and concise. It covers installation, configuration of all retry strategies, usage patterns with async/await, and real-world examples.

You can find the documentation here: Typhoon Documentation

Repository

Typhoon is fully open source and developed in the open. The repository contains the source code, tests, documentation, and example usage. The project has no external dependencies and is designed to be easy to audit, integrate, and extend. Contributions, issues, and discussions are very welcome.

Repository link: https://github.com/space-code/typhoon

If you find Typhoon useful and it fits your needs, please consider giving the repository a star on GitHub. It helps a lot with visibility and motivates further development.

Thanks for taking the time to check it out.

13 Likes

Looks very cool! I can also imagine hooking this up to Swift Configuration to set the retry count and interval from config files or env variables. Thanks for sharing!

2 Likes

Neat, always nice to have more small focused libs.

I would suggest that jitter isn’t really optional, in any bigger service you’ll want to have some amount of hitter, so I’d suggest just making it part of the normal strategies rather than having a separate “WithJitter” version.

You could also use Swift’s Duration type instead of pulling dispatch types; That’s the canonical way to express time duration nowadays.

7 Likes

Good points! Thanks for the feedback.

I agree that jitter is effectively required in most-real services. I initially made it optional to keep the core strategies minimal and explicit as possible, but folding jitter into the default behavior (or at least making it less “opt-in”) is definitely something I’ll reconsider.

Regarding Duration: you’re absolutely right that it’s the canonical way to represent time intervals in modern Swift. Unfortunately, it’s only available starting from iOS 16, while the library currently targets iOS 13 as a minimum. That’s the main reason for relying on Dispatch-based types for now.

Thanks again for the thoughtful suggestions much appreciated!

1 Like

In that case I’d make the enum internal and add static factory methods and make the Duration ones higher availability — keep your cake and eat it too :-)

3 Likes