Skip to content

lgvv/swift-singleflight

swift-singleflight

Swift iOS License

A Swift implementation of the singleflight concurrency primitive — suppressing redundant in-flight executions by coalescing concurrent callers onto a single shared Task.

Motivation

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.

Concurrency Model

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:

  1. If no Task is currently stored, a new one is created, stored, and awaited.
  2. If a Task is already in-flight, the incoming caller awaits the same task directly — no new work is spawned.
  3. Upon settlement (success or error), the stored Task is 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.

Installation

// Package.swift
dependencies: [
    .package(url: "https://github.com/geonwoolee/swift-singleflight.git", from: "1.0.0")
],
targets: [
    .target(
        name: "YourTarget",
        dependencies: ["Singleflight"]
    )
]

Usage

Singleflight — Actor-isolated, single key

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 == c

Errors are also shared: if the in-flight task throws, every waiting caller receives the same error.

SingleflightGroup — Actor-isolated, keyed

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.

Implementations

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.

Selecting a backend

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.

Platform policy

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.

Error Propagation

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.

Thread Safety

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 it nonisolated(unsafe) where required by the compiler, with the invariant that all reads and writes occur within the critical section.

Requirements

  • Swift 6+
  • iOS 13.0+

License

This project is licensed under the MIT License.

About

A Swift singleflight implementation — coalescing concurrent callers onto a single shared Task.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages