A zero-dependency Swift utility library for async/await concurrency, rate limiting, retry logic, circuit breakers, and more. Built with Swift 6.1 and structured concurrency.
Eight actor-based, thread-safe rate limiter implementations behind a shared RateLimiter protocol. All are Sendable and safe for concurrent use.
Classic token bucket algorithm. Tokens accumulate over time and allow controlled bursts.
let limiter = TokenBucketRateLimiter(capacity: 10, per: .seconds(1))
try await limiter.acquire()Requests drain at a fixed rate, smoothing out traffic spikes.
let limiter = LeakyBucketRateLimiter(capacity: 10, per: .seconds(1))
try await limiter.acquire()Counts requests within fixed time intervals. Simple and low-overhead.
let limiter = FixedWindowRateLimiter(limit: 100, window: .seconds(60))Tracks individual request timestamps for precise limiting. Most accurate, O(n) memory.
let limiter = SlidingWindowLogRateLimiter(limit: 100, window: .seconds(60))Hybrid approach that approximates a sliding window with O(1) memory.
let limiter = SlidingWindowCounterRateLimiter(limit: 100, window: .seconds(60))Semaphore-style limiter that caps the number of concurrent operations.
let limiter = ConcurrencyLimiter(maxConcurrent: 5)
try await limiter.withPermit {
// at most 5 concurrent executions
}Automatically adjusts its rate based on success/failure feedback from downstream services.
let limiter = AdaptiveRateLimiter(initialRate: 10.0, minRate: 1.0, maxRate: 100.0)
// call limiter.recordSuccess() or limiter.recordRateLimited() to adjustCombines multiple limiters using Swift parameter packs. A request must pass all of them.
let composite = CompositeRateLimiter(tokenBucket, fixedWindow)
try await composite.acquire()Per-key limiting (e.g. per-user, per-IP). Creates limiters on demand.
let keyed = KeyedRateLimiter { TokenBucketRateLimiter(capacity: 10, per: .seconds(1)) }
try await keyed.acquire(forKey: userId)Retry operations with configurable exponential backoff, jitter, typed throws, and cancellation support.
let result = try await withRetry(configuration: .default) {
try await fetchFromAPI()
}| Preset | Attempts | Initial Delay | Max Delay | Backoff | Jitter |
|---|---|---|---|---|---|
.default |
3 | 1s | 30s | 2.0x | 0.25 |
.aggressive |
5 | 0.5s | 60s | 2.0x | 0.25 |
.conservative |
10 | 2s | 120s | 3.0x | 0.5 |
Control which errors trigger retries with composable predicates.
try await withRetry(
predicate: .on(NetworkError.self).and(.except(AuthError.self))
) {
try await fetchFromAPI()
}Abort the entire retry sequence if a deadline is exceeded.
try await withRetry(configuration: .aggressive, timeout: .seconds(30)) {
try await fetchFromAPI()
}Combine retries with rate limiting to avoid hammering a struggling service.
let limiter = TokenBucketRateLimiter(capacity: 5, per: .seconds(1))
let result = try await withRetry(rateLimiter: limiter) {
try await fetchFromAPI()
}Prevent cascading failures by stopping calls to a failing dependency. Transitions through closed, open, and half-open states.
let breaker = CircuitBreaker(failureThreshold: 5, successThreshold: 2, timeout: .seconds(30))
let result = try await breaker.execute {
try await callExternalService()
}Apply rate limiting, circuit breaking, and retries in a single call.
try await withResilience(
rateLimiter: limiter,
circuitBreaker: breaker,
retryConfiguration: .default
) {
try await callExternalService()
}Poll an operation until a condition is met, with exponential backoff, jitter, timeout, and cancellation support.
let result = try await withPolling(
until: { $0.status == .completed },
operation: { try await checkJobStatus() }
)Configure attempts, delays, backoff, jitter, and an optional total timeout.
let config = PollingConfiguration(
maxAttempts: 20,
baseDelay: .milliseconds(500),
maxDelay: .seconds(10),
backoffMultiplier: 2.0,
jitterFactor: 0.5,
timeout: .seconds(60)
)
let result = try await withPolling(
configuration: config,
until: { $0.isReady },
operation: { try await pollServer() }
)Pass any Clock to control time in tests without real delays.
let result = try await withPolling(
configuration: .default,
clock: testClock,
until: { $0 == .done },
operation: { try await fetchStatus() }
)
## AsyncSequence Utilities
### Prepend and Append to AsyncSequence
Wrap any `AsyncSequence` with prefix and/or suffix elements or sequences. Zero-cost abstractions using `@inlinable`.
```swift
let wrapped = stream.wrapped(prefix: headerElement, suffix: trailerElement)
for await element in wrapped {
// headerElement, then all stream elements, then trailerElement
}Read files asynchronously in chunks using for await.
let handle = FileHandle(forReadingAtPath: "/path/to/file")!
for try await chunk in handle {
// chunk is ArraySlice<UInt8>, default 64KB
}Tap into an AsyncSequence to perform side effects on each element without transforming it.
let tapped = SideEffectAsyncSequence(base: stream, process: { element in
logger.log("Received: \(element)")
}, onFinish: {
logger.log("Stream complete")
})Run shell commands and external processes from Swift.
let output = try runCommand("ls -la")
let info = try runExternalCommand(executablePath: "/usr/bin/git", arguments: ["status"])
let probe = try runFfprobe(ffprobeArguments: ["-show_format", "video.mp4"])Read environment variables from the system or fall back to a .env file.
let apiKey = getEnvironmentVariable("API_KEY", from: envFileUrl)Pretty-print any Encodable value as formatted JSON.
let data = try prettyEncode(myStruct)Convert a Double to its raw [UInt8] byte representation.
let bytes = doubleToUInt8Array(3.14)- Swift 6.1+
- macOS 14.0+ / iOS 17.0+ / watchOS 7.0+ / tvOS 14.0+ / visionOS 1.0+
- No external dependencies
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/atacan/UsefulThings.git", branch: "main")
]Then add "UsefulThings" to the target dependencies:
.target(
name: "YourTarget",
dependencies: ["UsefulThings"]
)See LICENSE for details.