Cadence – rate limiting, debouncing & request coalescing (zero-dependency, Swift 6)

I've open-sourced Cadence, a small Swift 6 concurrency library for pacing
outbound requests. It came out of a weather app that hammers a strictly
rate-limited public API from an app, a widget, and a watch complication at once.

Three pieces, all behind one RequestScheduler actor:

  • Rate limitingSlidingWindowRateLimiter with any number of layered rules,
    each .wait or .throw, optional cross-launch persistence.
  • Debouncing — trailing-edge collapse of rapid repeats.
  • Request coalescing — merge concurrent similar requests into one call, split
    the result per caller via a RequestCombinable strategy.
let scheduler = RequestScheduler(
    rateLimiter: SlidingWindowRateLimiter(
        rules: [.perSecond(5), .perHour(240, behavior: .throw)]
    )
)
let user = try await scheduler.schedule { try await api.fetchUser(id: 42) }

Design notes that might interest this group:

  • Zero dependencies, and decoupled from any HTTP client — the API returns a
    generic T, so Cadence never sees your response type.
  • Deterministic tests via an injected CadenceClock; you assert how long the
    limiter waited without real time passing.
  • Swift 6 language mode, full strict concurrency; Apache-2.0.

Repo (README, DocC, examples): GitHub - peteroettl/Cadence: Actor-based request pacing for Swift concurrency: rate limiting, debouncing, and request coalescing. Zero dependencies. · GitHub

I'd love feedback — particularly on the RequestCombinable key/merge/split model
and whether the .wait/.throw per-rule design matches how you'd expect a limiter
to behave. Happy to hear where it breaks for your use cases.

3 Likes