ThrottleDebounce 3.0.0-beta3
ThrottleDebounce
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
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();
- Call
Throttler.Throttle
to throttle your delegate, orDebouncer.Debounce
to debounce it. PassAction action
/Func func
— your delegate to rate-limitTimeSpan wait
— how long to wait between executionsbool leading
—true
if the first invocation should be executed immediately, orfalse
if it should be queued. Optional, defaults totrue
for throttling andfalse
for debouncing.bool trailing
—true
if subsequent invocations in the waiting period should be enqueued for later execution once the waiting interval is over, orfalse
if they should be discarded. Optional, defaults totrue
.
- Call the resulting
RateLimitedAction
/RateLimitedFunc
object'sInvoke
method to enqueue an invocation.RateLimitedFunc.Invoke
will returndefault
(e.g.null
) ifleading
isfalse
and the rate-limitedFunc
has not been executed before. Otherwise, it will return theFunc
's most recent return value.
- Your delegate will be executed at the desired rate.
- Optionally call the
RateLimitedAction
/RateLimitedFunc
object'sDispose()
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
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
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
Retrier.Attempt(attempt => MyErrorProneAction(), new Retrier.Options { MaxAttempts = 2 });
Call Retrier.Attempt
.
- The first argument is an
Action<int>
orFunc<int, T>
, which is your delegate to attempt and possibly retry if it throws exceptions. The attempt number will be passed as theint
parameter, starting with0
before the first attempt, and1
before the first retry. If this delegate returns aTask
, it will be awaited to determine if it threw an exception. - The second argument is an optional
Options
struct that lets you define the limits and behavior of the retries, with the properties:int? MaxAttempts
— the total number of times the delegate is allowed to run in this invocation, equal to1
initial attempt plus at mostmaxAttempts - 1
retries if it throws an exception. Must be at least 1, if you set it to 0 it will clip to 1. Defaults tonull
, 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 ofRetrier.Attempt
and continuing across all attempts, rather than a time limit for each individual attempt. Defaults to null, which means attempts may continue for infinitely long.- If both
MaxAttempts
andMaxOverallDuration
are non-null, they will apply in conjunction — retries will continue if both the number of attempts is less thanMaxAttempts
and the total elapsed duration is less thanMaxOverallDuration
. - If both
MaxAttempts
andMaxOverallDuration
are null, Retrier will retry forever until the delegate returns without throwing an exception, orIsRetryAllowed
returnsfalse
.
- If both
Func<int, TimeSpan>? Delay
— how long to wait between attempts, as a function of the number of retries that have already run, starting with0
after the first attempt and before the first retry. You can return a constantTimeSpan
for a fixed delay, or pass longer values for subsequent attempts to implement, for example, exponential backoff. Optional, defaults tonull
, which means no delay. The minimum value is0
, the maximum value isint.MaxValue
(uint.MaxValue - 1
starting in .NET 6), and values outside this range will be clipped. Retrier will wait for this delay after callingAfterFailure
and before callingBeforeRetry
. You can experiment with and visualize different delay strategies and values on .NET Fiddle. Implementations you can pass:Retrier.Delays.Constant
Retrier.Delays.Linear
Retrier.Delays.Exponential
Retrier.Delays.Power
Retrier.Delays.Logarithm
Retrier.Delays.MonteCarlo
- any custom function that returns a
TimeSpan
Func<Exception, bool>? IsRetryAllowed
— whether the delegate is permitted to execute again after a givenException
instance. Returntrue
to allow retries orfalse
forRetrier.Attempt
to 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 almost all exceptions, but regardless of this property, Retrier never retries on anOutOfMemoryException
.Action<int, Exception>? AfterFailure
— a delegate to run extra logic after an attempt fails, if you want to log a message or perform any cleanup. Optional, defaults to not running anything. Theint
parameter is the attempt number that most recently failed, starting with0
the first time this delegate is called. The most recentException
is also passed. Runs before waiting forDelay
andBeforeRetry
.Action<int, Exception>? 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. Optional, defaults to not running anything. Theint
parameter is the attempt number that will be run next, starting with1
the first time this delegate is called. The most recentException
is also passed. Runs afterAfterFailure
and waiting forDelay
.CancellationToken? CancellationToken
— used to cancel the attempts and delays before they have all completed. Optional, defaults to no cancellation token. When cancelled,Attempt
throws aTaskCancelledException
.
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
.
Example
Send at most 5 HTTP requests, 2 seconds apart, until a successful response is received
using System.Net;
using ThrottleDebounce;
Retrier.Options options = new() {
MaxAttempts = 5,
Delay = Retrier.Delays.Constant(TimeSpan.FromSeconds(2)),
AfterFailure = (i, exception) => Console.WriteLine(exception is HttpRequestException { StatusCode: { } status } ? $"Received {(int) status} response (attempt #{i:N0})" : exception.Message),
BeforeRetry = (i, exception) => Console.WriteLine($"Retrying (attempt #{i:N0})")
};
using HttpClient httpClient = new();
HttpStatusCode statusCode = await Retrier.Attempt(async attempt => {
using HttpResponseMessage response = await httpClient.GetAsync("https://httpbin.org/status/200%2C500"); // randomly return 200 or 500
response.EnsureSuccessStatusCode(); // throws HttpRequestException for status codes outside the range [200, 300)
return response.StatusCode;
}, options);
Console.WriteLine($"Final response status code: {(int) statusCode}");
Received 500 response (attempt #0)
Retrying (attempt #1)
Received 500 response (attempt #1)
Retrying (attempt #2)
Final response status code: 200
Showing the top 20 packages that depend on ThrottleDebounce.
Packages | Downloads |
---|---|
Elsa.Workflows.Runtime
Provides workflow runtime functionality.
|
3 |
Elsa
Bundles the most commonly-used packages when building an Elsa workflows application.
|
2 |
Elsa.Workflows.Runtime
Provides workflow runtime functionality.
|
2 |
Elsa
Bundles the most commonly-used packages when building an Elsa workflows application.
|
1 |
Elsa.Workflows.Runtime
Provides workflow runtime functionality.
|
1 |
.NET Framework 4.5.2
- No dependencies.
.NET Standard 2.0
- No dependencies.
Version | Downloads | Last updated |
---|---|---|
3.0.0-beta3 | 1 | 2025/6/8 |
3.0.0-beta2 | 3 | 2025/5/25 |
3.0.0-beta1 | 1 | 2025/5/27 |
2.0.1 | 2 | 2025/5/28 |
2.0.0 | 1 | 2025/5/28 |
2.0.0-SNAPSHOT-2 | 1 | 2025/5/27 |
2.0.0-SNAPSHOT | 1 | 2025/5/27 |
1.0.3 | 1 | 2025/5/27 |
1.0.2 | 1 | 2025/5/27 |