singleflight.Group suppresses duplicate function executions only while an operation for the same key is in flight. Concurrent callers can receive one shared result, but a caller arriving after completion starts a new execution. The group is therefore a request-coalescing mechanism, not a result cache.

This boundary affects cache fills, metadata refreshes, backend reads, and other keyed operations that can attract bursts of identical concurrent work. A group can reduce simultaneous pressure on the backing operation without extending the lifetime of its returned value.

One key identifies one active execution

A Group maintains a namespace of string keys. Do registers an execution for a key and runs the supplied function. If another Do call with the same key arrives before that execution completes, the duplicate waits and receives the same value and error.

Calls with different keys do not share results. The suppression boundary is both key-specific and temporal: equality of keys matters only while an execution remains active.

var g singleflight.Group

v, err, shared := g.Do("account:42", func() (any, error) {
    return loadAccount(42)
})

The shared result reports that the returned value was supplied to multiple callers. It does not indicate that the value came from persistent storage inside the group.

Once the active call finishes and its entry is removed, another call using "account:42" invokes its function again.

Completion removes the coalescing window

A cache associates a key with a value for some retention period. singleflight associates a key with an active call. Those lifetimes are materially different.

Consider two requests separated by the completion of the first operation:

request A ---- execute ---- result
request B ------ wait ----- result
                              request C ---- execute ---- result

Requests A and B overlap, so they can share one execution. Request C arrives after the first execution has completed, so it creates another one even though the key is identical.

This behavior makes singleflight useful beside a cache rather than as a replacement for one. A common cache-fill path first checks stored state, then coalesces concurrent misses around the backing read. The cache determines value retention; the group limits duplicate work during the miss window.

Errors are shared with duplicate callers

The shared outcome includes both the value and the error. If the active function returns an error, duplicate callers waiting on the same key receive that error as part of the same result.

That property can temporarily collapse a burst of failing backend operations. It does not create an error cache. After the failed execution completes, a later call can start the function again immediately.

This distinction matters during outages. Coalescing can reduce concurrent amplification while a slow failing call remains active, but it does not impose retry spacing after completion. Backoff, rate limiting, circuit breaking, or negative caching remain separate policies.

Do ties duplicate callers to the active call

Do is synchronous. A duplicate caller waits until the active function returns. The package does not accept a context in the Do signature, so cancellation of an individual duplicate is not represented directly by that method.

DoChan exposes completion through a receive-only channel instead. The returned channel receives one Result and is not closed. A caller can combine that channel with other channel operations, including a context cancellation signal, without changing the lifetime of the active shared execution.

resultCh := g.DoChan(key, func() (any, error) {
    return fetch(key)
})

select {
case result := <-resultCh:
    use(result.Val, result.Err)
case <-ctx.Done():
    return ctx.Err()
}

Leaving the wait does not itself cancel the function registered in the group. Cancellation policy for the underlying operation has to be designed separately from duplicate suppression.

Forget permits overlap for the same key

Forget removes the association between a key and its current in-flight call. A later Do for that key can then start a new function instead of joining the earlier execution.

The earlier execution is not canceled by Forget. It continues independently. As a result, calling Forget while work is active can create two simultaneous executions for the same logical key.

That behavior is useful when an existing call should no longer attract new duplicates, but it changes the central one-active-call property for that key. Code using Forget needs to tolerate overlapping operations and potentially different completion order.

For mutation-adjacent work, that overlap can be significant. Duplicate suppression does not provide serialization, transaction ordering, or exclusion after a key is forgotten.

Key design defines the sharing domain

The group treats keys as opaque strings. Application code decides which operations are equivalent enough to share one result.

A key that is too broad can merge calls whose inputs differ in relevant state. A key that is too narrow can leave substantial duplicate work unsuppressed. Parameters that affect the returned value, authorization scope, tenant identity, representation, or backend selection may need to participate in the key.

The group does not inspect the function or compare its captured arguments. Two callers using the same key can provide different functions; if their calls overlap, the duplicate receives the result of the function already registered for that key.

That makes key construction part of correctness, not merely an optimization detail.

Suppression changes load shape, not value lifetime

A hot key can turn many concurrent callers into one backend execution plus a set of waiters. This reduces duplicate work during the active interval, but it also couples those callers to the latency and outcome of the selected execution.

After completion, the coupling disappears. The next caller creates a fresh interval and may become the execution that subsequent duplicates join.

singleflight.Group therefore has a narrow contract: one in-flight execution per key under normal Do use, shared completion for overlapping duplicates, and no retained result after that execution leaves the group. Cache lifetime, retry policy, cancellation ownership, and durable serialization remain outside that contract.