ThrottleDebounce 3.0.0-beta4

This is a prerelease version of ThrottleDebounce.
dotnet add package ThrottleDebounce --version 3.0.0-beta4
                    
NuGet\Install-Package ThrottleDebounce -Version 3.0.0-beta4
                    
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="ThrottleDebounce" Version="3.0.0-beta4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ThrottleDebounce" Version="3.0.0-beta4" />
                    
Directory.Packages.props
<PackageReference Include="ThrottleDebounce" />
                    
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 ThrottleDebounce --version 3.0.0-beta4
                    
#r "nuget: ThrottleDebounce, 3.0.0-beta4"
                    
#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.
#addin nuget:?package=ThrottleDebounce&version=3.0.0-beta4&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ThrottleDebounce&version=3.0.0-beta4&prerelease
                    
Install as a Cake Tool

ThrottleDebounce

Package Version NuGet Gallery Download Count GitHub Workflow Status Testspace Coveralls

Rate-limit your actions and funcs by throttling and debouncing them. Retry when an exception is thrown.

This is a .NET library that lets you rate-limit delegates so they are only executed at most once in a given interval, even if they are invoked multiple times in that interval. You can also invoke a delegate and automatically retry it if it fails.

Installation

This package is available on NuGet Gallery.

dotnet add package ThrottleDebounce

It targets .NET Standard 2.0 and .NET Framework 4.5.2, so it should be compatible with many runtimes.

Rate limiting

Usage

using ThrottleDebounce;

Action originalAction;
Func<int> originalFunc;

TimeSpan wait = TimeSpan.FromMilliseconds(50);
using RateLimitedAction throttledAction = Throttler.Throttle(originalAction, wait, leading: true, trailing: true);
using RateLimitedFunc<int> debouncedFunc = Debouncer.Debounce(originalFunc, wait, leading: false, trailing: true);

throttledAction.Invoke();
int? result = debouncedFunc.Invoke();
  1. Call Throttler.Throttle to throttle your delegate, or Debouncer.Debounce to debounce it. Pass
    1. Action action/Func func — your delegate to rate-limit
    2. TimeSpan wait — how long to wait between executions
    3. bool leadingtrue if the first invocation should be executed immediately, or false if it should be queued. Optional, defaults to true for throttling and false for debouncing.
    4. bool trailingtrue if subsequent invocations in the waiting period should be enqueued for later execution once the waiting interval is over, or false if they should be discarded. Optional, defaults to true.
  2. Call the resulting RateLimitedAction/RateLimitedFunc object's Invoke method to enqueue an invocation.
    • RateLimitedFunc.Invoke will return default (e.g. null) if leading is false and the rate-limited Func has not been executed before. Otherwise, it will return the Func's most recent return value.
  3. Your delegate will be executed at the desired rate.
  4. Optionally call the RateLimitedAction/RateLimitedFunc object's Dispose() method to prevent all queued executions from running when you are done.

Understanding throttling and debouncing

Summary

Throttling and debouncing both restrict a function to not execute too often, no matter how frequently you invoke it.

This is useful if the function is invoked very frequently, like whenever the mouse moves, but you don't want to it to run every single time the pointer moves 1 pixel, because the function is expensive, such as rendering a user interface.

Throttling allows the function to still be executed periodically, even with a constant stream of invocations.

Debouncing prevents the function from being executed at all until it hasn't been invoked for a while.

An invocation can result in at most one execution. For example, if both leading and trailing are true, one single invocation will execute once on the leading edge and not on the trailing edge.

Not all extra invocations are queued to run on the trailing edge — only the latest extra invocation is saved, and the other extras are dropped. For example, if you throttle mouse movement and then quickly move your pointer across your screen, only a few of the move event callbacks will be executed, many pixels apart; it won't slowly execute thousands of callbacks all spread out over a long time.

Diagram

Strategies for Rate-Limiting

Lodash documentation
Article and demo

Debouncing and Throttling Explained Through Examples by David Corbacho

Examples

Throttle an action to execute at most every 1 second
using ThrottleDebounce;

Action throttled = Throttler.Throttle(() => Console.WriteLine("hi"), TimeSpan.FromSeconds(1)).Invoke;

throttled(); //logs at 0s
throttled(); //logs at 1s
Thread.Sleep(1000);
throttled(); //logs at 2s
Debounce a function to execute after no invocations for 200 milliseconds
Func<double, double, double> debounced = Debouncer.Debounce((double x, double y) => Math.Sqrt(x * x + y * y),
    TimeSpan.FromMilliseconds(200)).Invoke;

double? result;
result = debounced(1, 1); //never runs
result = debounced(2, 2); //never runs
result = debounced(3, 4); //runs at 200ms
Canceling a rate-limited action so any queued executions won't run
RateLimitedAction rateLimited = Throttler.Throttle(() => Console.WriteLine("hello"), TimeSpan.FromSeconds(1));

rateLimited.Invoke(); //runs at 0s
rateLimited.Dispose();
rateLimited.Invoke(); //never runs
Save a WPF window's position to the registry at most every 1 second
static void SaveWindowLocation(double x, double y) => Registry.SetValue(@"HKEY_CURRENT_USER\Software\My Program", 
    "Window Location", $"{x},{y}");

Action<double, double> saveWindowLocationThrottled = Throttler.Throttle<double, double>(saveWindowLocation, 
    TimeSpan.FromSeconds(1)).Invoke;

LocationChanged += (sender, args) => SaveWindowLocationThrottled(Left, Top);
Prevent accidental double-clicks on a WPF button
public MainWindow(){
    InitializeComponent();

    Action<object, RoutedEventArgs> onButtonClickDebounced = Debouncer.Debounce<object, RoutedEventArgs>(
        OnButtonClick, TimeSpan.FromMilliseconds(40), true, false).Invoke;

    MyButton.Click += new RoutedEventHandler(onButtonClickDebounced);
}

private void OnButtonClick(object sender, RoutedEventArgs e) {
    MessageBox.Show("Button clicked");
}

Retrying

Given a function or action, you can execute it and, if it threw an exception, automatically execute it again until it succeeds.

Usage

using ThrottleDebounce.Retry;

var result = Retrier.Attempt(attempt => MyErrorProneFunc(), new Options { MaxAttempts = 2 });

Call Retrier.Attempt.

  1. The first argument is an Action<long> or Func<long, T>, which is your delegate to attempt and possibly retry if it throws exceptions. The attempt number will be passed as the long parameter, starting with 0 before the first attempt, and 1 before the first retry. If this delegate returns a task, it will be awaited to determine if it threw an exception.
  2. The second argument is an optional Options or Options.Async struct that lets you define the limits and behavior of the retries, with the properties:
    • long? MaxAttempts — the total number of times the delegate is allowed to run in this invocation, equal to 1 initial attempt plus at most maxAttempts - 1 retries if it throws an exception. Must be at least 1; if you set it to 0 it will clip to 1. Defaults to null, which means infinitely many retries.
    • TimeSpan? MaxOverallDuration — the total amount of time that Retrier is allowed to spend on attempts. This is the cumulative duration starting from the invocation of Retrier.Attempt and continuing across all attempts, including delays, rather than a time limit for each individual attempt. Defaults to null, which means attempts may continue for infinitely long.
      • If both MaxAttempts and MaxOverallDuration are non-null, they will apply in conjunction — retries will continue iff both the number of attempts is less than MaxAttempts and the total elapsed duration is less than MaxOverallDuration.
      • If both MaxAttempts and MaxOverallDuration are null, Retrier will retry forever until the delegate returns without throwing an exception, or IsRetryAllowed returns false.
    • Func<long, TimeSpan>? Delay — how long to wait between attempts, as a function of the number of retries that have already run, starting with 0 after the failed first attempt and before the first retry. You can return a constant TimeSpan for a fixed delay, or pass longer values for subsequent attempts to implement, for example, exponential backoff. Optional, defaults to null, which means no delay. The minimum value is 0, the maximum value is int.MaxValue (uint.MaxValue - 1 in .NET ≥ 6), and values outside this range will be clipped. Retrier will wait for this delay after calling AfterFailure and before calling BeforeRetry. You can experiment with and visualize different delay strategies and values on .NET Fiddle. Implementations you can pass:
      • Delays.Constant
      • Delays.Linear
      • Delays.Exponential
      • Delays.Power
      • Delays.Logarithmic
      • Delays.MonteCarlo
      • any custom function that returns a TimeSpan
    • Func<Exception, long, bool>? IsRetryAllowed — whether the delegate is permitted to execute again after a given Exception instance and attempt number, starting with 0. Return true to allow another retry, or false for Retrier.Attempt to abort and throw the disallowed exception. For example, you may want to retry after HTTP 500 errors since subsequent requests may succeed, but stop after the first failure for an HTTP 403 error which probably won't succeed if the same request is sent again. Optional, null defaults to retrying on all exceptions, but regardless of this property, Retrier never retries on an OutOfMemoryException. If your attempt func is asynchronous, you may specify an asynchronous IsRetryAllowed by creating an Options.Async instead of Options.
    • Action<Exception, long>? AfterFailure — a delegate to run extra logic after an attempt fails, if you want to log a message or perform any cleanup. Similar to BeforeRetry except it runs before waiting for Delay instead of after. Optional, defaults to not running anything. The long parameter is the attempt number that most recently failed, starting with 0 the first time this action is called. The most recent Exception is also passed. Runs before waiting for Delay and BeforeRetry. If your attempt func is asynchronous, you may specify an asynchronous AfterFailure by creating an Options.Async instead of Options.
    • Action<Exception, long>? BeforeRetry — a delegate to run extra logic before a retry attempt, for example, if you want to log a message or perform any cleanup before the next attempt. Similar to AfterFailure except it runs after waiting for Delay instead of before. Optional, defaults to not running anything. The long parameter is the attempt number that will be run next, starting with 1 the first time this action is called. The most recent Exception is also passed. Runs after both AfterFailure and waiting for Delay. If your attempt func is asynchronous, you may specify an asynchronous BeforeRetry by creating an Options.Async instead of Options.
    • CancellationToken? CancellationToken — used to cancel the attempts and delays before they have all completed. Optional, defaults to no cancellation token. When cancelled, Attempt throws a TaskCancelledException.
Asynchrony

If the delegate Func returns a Task or Task<T>, Retrier will await it to determine if it threw an exception. In this case, you should await Retrier.Attempt to get the final return value or exception.

Return value

If your delegate runs successfully without throwing an exception, Attempt will return your delegate Func's return value, or void if the delegate is an Action that doesn't return anything.

Exceptions

If Retrier ran out of attempts or time to retry, it will rethrow the last exception thrown by the delegate, or, if Options.CancellationToken was canceled, a TaskCanceledException.

Sequence
  1. Run delegate (attempt #0)
  2. Run AfterFailure (attempt #0)
  3. Check IsRetryAllowed (attempt #0)
  4. Wait for Delay (attempt #0)
  5. Run BeforeRetry (attempt #1)
  6. Run delegate (attempt #1)
  7. Run AfterFailure (attempt #1)
  8. Check IsRetryAllowed (attempt #1)
  9. Wait for Delay (attempt #1)
  10. Run BeforeRetry (attempt #2)
  11. etc.

Example

Send at most 5 HTTP requests, 2 seconds apart, until a successful response is received
using System.Net;
using ThrottleDebounce.Retry;

using HttpClient httpClient = new();

try {
    HttpStatusCode statusCode = await Retrier.Attempt(async attempt => {
        using HttpResponseMessage response = await httpClient.GetAsync("https://httpbin.org/status/200%2C500");
        response.EnsureSuccessStatusCode(); // throws HttpRequestException for status codes outside the range [200, 300)
        return response.StatusCode;
    }, new Options {
        MaxAttempts = 5,
        Delay       = Delays.Constant(TimeSpan.FromMilliseconds(100)),
        AfterFailure = (exception, attempt) => Console.WriteLine(exception is HttpRequestException { StatusCode: { } status }
            ? $"Received {(int) status} response (attempt #{attempt:N0})" : exception.Message),
        BeforeRetry = (exception, attempt) => Console.WriteLine($"Retrying (attempt #{attempt:N0})")
    });

    Console.WriteLine($"Final response status code: {statusCode}");
} catch (HttpRequestException) {
    Console.WriteLine("All requests failed");
}
Received 500 response (attempt #0)
Retrying (attempt #1)
Received 500 response (attempt #1)
Retrying (attempt #2)
Final response status code: 200
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 net452 is compatible.  net46 was computed.  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.
  • .NETFramework 4.5.2

    • No dependencies.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages (82)

Showing the top 5 NuGet packages that depend on ThrottleDebounce:

Package Downloads
Elsa

Bundles the most commonly-used packages when building an Elsa workflows application.

Elsa.Workflows.Runtime

Provides workflow runtime functionality.

Elsa.EntityFrameworkCore

Provides Entity Framework Core implementations of various abstractions from various modules.

Elsa.Alterations.Core

Provides core interfaces, models and services that support the alteration engine.

Elsa.Alterations

Provides alterations for workflow instances.

GitHub repositories (2)

Showing the top 2 popular GitHub repositories that depend on ThrottleDebounce:

Repository Stars
elsa-workflows/elsa-core
A .NET workflows library
elsa-workflows/elsa-studio
A modular, extensible dashboard application framework
Version Downloads Last Updated
3.0.0-beta4 98 6/21/2025
3.0.0-beta3 189 6/3/2025
3.0.0-beta2 330 3/29/2025
3.0.0-beta1 223 2/12/2025
2.0.1 7,037 3/21/2025
2.0.0 291,314 7/8/2022
2.0.0-SNAPSHOT-2 201 7/8/2022
2.0.0-SNAPSHOT 213 6/29/2022
1.0.3 98,184 9/11/2020
1.0.2 1,106 11/15/2019