A Swift implementation of the singleflight concurrency primitive — suppressing redundant in-flight executions by coalescing concurrent callers onto a single shared
Task.
In concurrent systems, it is common for multiple independent callers to request the same resource simultaneously. Without coordination, each caller spawns a separate execution — a phenomenon known as a thundering herd or cache stampede — resulting in redundant network I/O, unnecessary CPU cycles, and increased backend pressure.
The singleflight pattern, originally popularized by Go's golang.org/x/sync/singleflight package, addresses this by serializing duplicate in-flight requests: only the first caller triggers the actual work, while subsequent callers for the same operation suspend and await the same result. Once the operation settles — whether by returning a value or throwing an error — all waiting callers are unblocked simultaneously.
swift-singleflight brings this primitive to Swift's structured concurrency model, with multiple synchronization backends suited to different deployment targets and threading constraints.
The core invariant is straightforward: at most one Task for a given operation is live at any point in time.
Caller A ──┐
Caller B ──┼──► [ Singleflight ] ──► work() ──► result
Caller C ──┘ │ │
└───────────────── shared ◄────┘
When execute(_:) is called:
- If no
Taskis currently stored, a new one is created, stored, and awaited. - If a
Taskis already in-flight, the incoming caller awaits the same task directly — no new work is spawned. - Upon settlement (success or error), the stored
Taskis cleared, making the instance ready for the next execution cycle.
This guarantee holds under Swift 6's strict concurrency model: all implementations are Sendable, and state mutation is confined to a single synchronization domain per instance.
// Package.swift
dependencies: [
.package(url: "https://github.com/geonwoolee/swift-singleflight.git", from: "1.0.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["Singleflight"]
)
]import Singleflight
let singleFlight = Singleflight<Data>()
// Three concurrent callers — only one invocation of fetchData() occurs
async let r1 = singleFlight.execute { try await fetchData() }
async let r2 = singleFlight.execute { try await fetchData() }
async let r3 = singleFlight.execute { try await fetchData() }
let (a, b, c) = try await (r1, r2, r3) // a == b == cErrors are also shared: if the in-flight task throws, every waiting caller receives the same error.
SingleflightGroup extends the primitive with a key-indexed namespace. Operations sharing a key are coalesced; operations with distinct keys execute concurrently and independently.
import Singleflight
let group = SingleflightGroup<String, User>()
// Concurrent callers with the same key share one execution
async let user1 = group.execute(key: "uid-42") { try await fetchUser("42") }
async let user2 = group.execute(key: "uid-42") { try await fetchUser("42") } // coalesced
// A different key runs independently, in parallel
async let user3 = group.execute(key: "uid-99") { try await fetchUser("99") }This makes SingleflightGroup well-suited for keyed cache layers, where the key typically encodes a resource identifier or query fingerprint.
Six implementations are provided. All expose the identical execute(_:) API, differing only in their underlying primitive and availability:
| Type | Primitive | Availability | Notes |
|---|---|---|---|
Singleflight |
actor |
iOS 13+ | Recommended default. Integrates natively with Swift concurrency. |
SingleflightGroup |
actor |
iOS 13+ | Keyed variant of Singleflight. |
SingleflightNSLock |
NSLock |
iOS 13+ | Foundation lock. Sendable across concurrency domains. |
SingleflightUnfairLock |
os_unfair_lock |
iOS 13+ | Lowest-overhead option. Priority-inversion safe. |
SingleflightAllocatedUnfairLock |
OSAllocatedUnfairLock |
iOS 16+ | Heap-allocated unfair lock with modern ergonomics. |
SingleflightMutex |
Mutex (Synchronization) |
iOS 18+ | Swift 6 Synchronization module. Statically enforces non-reentrant access. |
Actor-based (Singleflight, SingleflightGroup) — the idiomatic choice for codebases built on Swift structured concurrency. Actor isolation provides data-race safety without manual locking, and the compiler enforces correct usage at the call site.
Lock-based variants — appropriate when the instance must be stored in a non-isolated context (e.g., a nonisolated property or a @unchecked Sendable type) where actor hops would introduce unacceptable latency. The lock is held only during the brief critical section of task lookup and assignment — never across an await — so contention is minimal.
SingleflightMutex — leverages the Mutex<T> type from the Synchronization module introduced in Swift 6. Unlike the other lock-based variants, Mutex statically prevents the lock from being captured across a suspension point, making misuse a compile-time error rather than a runtime hazard. Recommended for iOS 18+ targets where this guarantee is desirable.
swift-singleflight is currently maintained as an iOS-first package:
| Minimum deployment target | iOS 13.0 |
| Toolchain | Swift 6 |
| CI target | iOS Simulator on GitHub Actions (macOS runner) |
Some implementations require newer iOS versions:
SingleflightAllocatedUnfairLock: iOS 16+SingleflightMutex: iOS 18+
Other platforms (macOS/tvOS/watchOS/Linux) may be added in later releases.
Errors are propagated to all concurrent callers, not just the one whose closure threw. This is a deliberate design choice: since all callers are awaiting the same Task, the task's thrown error is observed by every caller upon await task.value. The Task is then cleared, allowing the next call to execute(_:) to initiate a fresh execution.
All implementations satisfy Swift 6's Sendable requirements:
- Actor-based types use actor isolation as their synchronization domain.
- Lock-based types confine mutable state (
task) behind a lock and annotate itnonisolated(unsafe)where required by the compiler, with the invariant that all reads and writes occur within the critical section.
- Swift 6+
- iOS 13.0+
This project is licensed under the MIT License.