SingleFlight 1.0.0

dotnet add package SingleFlight --version 1.0.0
                    
NuGet\Install-Package SingleFlight -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SingleFlight" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SingleFlight" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="SingleFlight" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SingleFlight --version 1.0.0
                    
#r "nuget: SingleFlight, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SingleFlight@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SingleFlight&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=SingleFlight&version=1.0.0
                    
Install as a Cake Tool

<div align="center"> <img src="assets/icon.png" alt="SingleFlight"> </div>

SingleFlight

NuGet License: MIT .NET Standard 2.0 main Tests: 24 passing codecov

A simple, lightweight, zero-dependency .NET Package that stops duplicate concurrent calls: identical in-flight work runs just once and every caller shares the result.

Requirements

Targets .NET Standard 2.0, so it runs on virtually any modern project:

  • .NET / .NET Core 2.0 and later (including .NET 5 – .NET 11)
  • .NET Framework 4.6.1 and later
  • Mono, Xamarin, and Unity

Installation

dotnet add package SingleFlight

Or via the Package Manager Console:

Install-Package SingleFlight

What it does

When many callers ask for the same thing at the same time — a cache miss stampede, a burst of identical HTTP requests, a hot config reload — you usually want the expensive work to run once and have everyone share that single result. That is the SingleFlight pattern (borrowed from Go's golang.org/x/sync/singleflight).

SingleFlight coalesces concurrent calls that share a key: the first caller runs the factory, every caller that arrives while the work is still in flight waits for and receives that same result.

It is not a cache. An entry only lives while its call is in flight. As soon as the shared execution finishes (successfully or not) the key is released, so the next call starts fresh.

The simplest entry point is the static class SingleFlight<T> with RunAsync, which returns the value directly (T is the type produced by the work). For isolated scopes or dependency injection there is also the instance-based SingleFlightGroup<T>, and a RunDetailedAsync variant that reports whether a call was coalesced — both covered below.

Usage

Wrap the expensive work with a key at a single call site:

using SingleFlight;

User user = await SingleFlight<User>.RunAsync($"user:{id}", () => LoadUserAsync(id));

That is the whole API. You call it once, from one place — the coalescing happens on its own: whenever several callers reach this line for the same key at the same time, LoadUserAsync runs a single time and they all receive that same result.

A typical cache-stampede guard:

async Task<User> GetUserAsync(int id)
{
    if (_cache.TryGetValue(id, out User cached))
        return cached;

    // Only one call hits the database per key, even under a burst of misses.
    return await SingleFlight<User>.RunAsync($"user:{id}", async () =>
    {
        var user = await _repository.LoadAsync(id);
        _cache.Set(id, user);
        return user;
    });
}

Keys are scoped per T: SingleFlight<User> and SingleFlight<Order> never coalesce with each other, even for an equal key.

Knowing whether a call was coalesced

RunDetailedAsync returns a SingleFlightResult<T> that carries both the value and a Joined flag — true when this caller attached to a call that was already in flight (it did not run the factory), false when this caller owned the execution. Useful for metrics, logging, or deciding whether you are responsible for a follow-up side effect:

var (user, joined) = await SingleFlight<User>.RunDetailedAsync($"user:{id}", () => LoadUserAsync(id));
if (!joined)
    _metrics.RecordCacheStampedeAvoided();

Isolated scopes and dependency injection

The static SingleFlight<T> is a single process-wide scope per T. When you want isolated keyspaces — so unrelated features can't collide on an equal key — or you want to inject and unit-test the coalescer, use SingleFlightGroup<T> (which implements ISingleFlightGroup<T>):

// Registration
services.AddSingleton<ISingleFlightGroup<User>, SingleFlightGroup<User>>();

// Usage
public UserService(ISingleFlightGroup<User> flight) => _flight = flight;

Task<User> GetUserAsync(int id) =>
    _flight.RunAsync($"user:{id}", () => LoadUserAsync(id));

Each group owns its own set of in-flight keys, so two groups never coalesce with each other even for an equal key. You can also pass a custom key comparer — e.g. new SingleFlightGroup<User>(StringComparer.OrdinalIgnoreCase).

Exceptions

If the factory fails, every caller joined to that call observes the same exception, and the key is released so the next call is free to retry.

API reference

Member Description
SingleFlight<T>.RunAsync(string key, Func<Task<T>> factory) Runs the factory once per in-flight key; concurrent callers with the same key share the returned value.
SingleFlight<T>.RunDetailedAsync(string key, Func<Task<T>> factory) Like RunAsync, but returns a SingleFlightResult<T> reporting whether this caller Joined an in-flight call.
SingleFlightGroup<T> / ISingleFlightGroup<T> An isolated, injectable coalescing scope with its own keyspace. Exposes the same RunAsync / RunDetailedAsync, plus a constructor taking a custom key comparer.
SingleFlightResult<T> Readonly struct carrying Value and Joined; deconstructs to (value, joined).

Contributing

Issues and pull requests are welcome. Please make sure dotnet test passes and warnings stay at zero before submitting.

License

MIT - see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 54 8/17/2026