Skip to content
Stampede protection

Stampede protection

When a hot entry expires, every concurrent request misses at once and they all run the action. ActionCache coalesces them: the first request executes while the rest wait, then re-read the cache and reuse what it stored.

This is on by default.

Opting out

Turn it off per endpoint when the action has per-request side effects that must not be skipped:

[HttpGet("forecasts")]
[ActionCache(Namespace = "Forecasts", SingleFlight = false)]
public IActionResult Get() => Ok(_forecasts);

In-process by default

Coalescing is per process, matching .NET’s HybridCache. With several instances running, each executes the action once.

Across instances

builder.Services.AddActionCache(options =>
{
    options.UseRedisCache("localhost:6379");
    options.UseDistributedSingleFlight();
});

This requires Redis or SQL Server — Redis is preferred when both are registered — and throws at startup if neither is present, rather than silently degrading to in-process coalescing.

Every cache miss then costs a lock round-trip to the backend, which is why it is not the default.

Timing out

A waiter blocks for at most ActionCacheSingleFlightOptions.WaitTimeout (default 10 seconds). If the lock cannot be acquired in that time the request executes uncoalesced rather than failing — consistent with the fail-open stance.

LeaseDuration (default 30 seconds) is how long the leader may hold the lock before other callers may assume it died. It must comfortably exceed the slowest action the cache fronts: if the lease expires while the leader is still running, a waiter acquires the lock and runs the action too — the stampede this exists to prevent.

Only backends whose locks carry a time-to-live enforce the lease — that is Redis, where it is the lock key’s TTL. The in-process semaphore and SQL Server’s session-scoped sp_getapplock hold until released, and a process that dies releases its lock by exiting.

Configuration is validated at startup: both values must be positive, and the lease must be longer than the wait. Otherwise a caller that waited the full timeout would always find the lease expired, and every slow request would stampede.