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 limiting —
SlidingWindowRateLimiterwith any number of layered rules,
each.waitor.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 aRequestCombinablestrategy.
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
genericT, 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.