From 34e327fa0055e7c11e2a24e7328d42137368bab5 Mon Sep 17 00:00:00 2001 From: David Pine Date: Fri, 9 Sep 2022 14:13:34 -0500 Subject: [PATCH 01/13] Draft and initial bits --- docs/core/extensions/http-ratelimiter.md | 32 +++++++++++++++ .../http/ClientSideRateLimitedHandler.cs | 40 +++++++++++++++++++ .../snippets/ratelimit/http/GlobalUsings.cs | 3 ++ .../snippets/ratelimit/http/Program.cs | 39 ++++++++++++++++++ .../snippets/ratelimit/http/http.csproj | 14 +++++++ 5 files changed, 128 insertions(+) create mode 100644 docs/core/extensions/http-ratelimiter.md create mode 100644 docs/core/extensions/snippets/ratelimit/http/ClientSideRateLimitedHandler.cs create mode 100644 docs/core/extensions/snippets/ratelimit/http/GlobalUsings.cs create mode 100644 docs/core/extensions/snippets/ratelimit/http/Program.cs create mode 100644 docs/core/extensions/snippets/ratelimit/http/http.csproj diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md new file mode 100644 index 0000000000000..85a4cabc2a699 --- /dev/null +++ b/docs/core/extensions/http-ratelimiter.md @@ -0,0 +1,32 @@ +--- +title: Rate limiting an HTTP handler in .NET +description: Learn how to create a client-side HTTP handler that limits the number of requests. +author: IEvangelist +ms.author: dapine +ms.date: 09/09/2022 +--- + +# Rate limiting an HTTP handler in .NET + +In this article, you'll learn how to create a client-side HTTP handler that rate limits the number of requests it sends. In this example, you'll see an that accesses the `"www.example.com"` resource. Resources are consumed by apps that rely on them, and when an app makes too many requests to a single resource this can lead to resource contention. Resource contention is when a resource is consumed by too many apps, and the resource is unable to serve all of the apps that are requesting it. This can lead to a poor user experience, and in some cases, it can even lead to a denial of service (DoS) attack. For more information on DoS, see [OWASP: Denial of Service](https://owasp.org/www-community/attacks/Denial_of_Service). + +## What is rate limiting? + +Rate limiting is the concept of limiting how much a resource can be accessed. For example, you may know that a database your app accesses can safely handle 1,000 requests per minute, but it may not handle much more than that. You can put a rate limiter in your appl that only allows 1,000 requests every minute and rejects any more requests before they can access the database. Thus, rate limiting your database and allowing your app to handle a safe number of requests. This is a common pattern in distributed systems, where you may have multiple instances of an app running, and you want to ensure that they don't all try to access the database at the same time. There are multiple different rate-limiting algorithms to control the flow of requests. + +## Implement a `DelegatingHandler` subclass + +To control the flow of requests, you implement a custom subclass. This is a type of that allows you to intercept and handle requests before they are sent to the server. You can also intercept and handle responses before they are returned to the caller. In this example, you'll implement a custom `DelegatingHandler` subclass that limits the number of requests that can be sent to a single resource. Consider the following custom `ClientSideRateLimitedHandler` class: + +:::code language="csharp" source="snippets/ratelimit/http/http/ClientSideRateLimitedHandler.cs"::: + +The preceding C# code: + +- Inherits the `DelegatingHandler` type. +- Implements the interface. +- Defines a `RateLimiter` field. + +## See also + +- [Announcing Rate Limiting for .NET](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet) +- [Rate limiting middleware in ASP.NET Core](/aspnet/core/performance/rate-limit) diff --git a/docs/core/extensions/snippets/ratelimit/http/ClientSideRateLimitedHandler.cs b/docs/core/extensions/snippets/ratelimit/http/ClientSideRateLimitedHandler.cs new file mode 100644 index 0000000000000..eb776f0c6f5e1 --- /dev/null +++ b/docs/core/extensions/snippets/ratelimit/http/ClientSideRateLimitedHandler.cs @@ -0,0 +1,40 @@ +internal sealed class ClientSideRateLimitedHandler + : DelegatingHandler, IAsyncDisposable +{ + private readonly RateLimiter _rateLimiter; + + public ClientSideRateLimitedHandler(RateLimiter limiter) + : base(new HttpClientHandler()) => _rateLimiter = limiter; + + protected override async Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + using RateLimitLease lease = await _rateLimiter.WaitAsync( + permitCount: 1, cancellationToken); + + if (lease.IsAcquired) + { + return await base.SendAsync(request, cancellationToken); + } + + var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests); + if (lease.TryGetMetadata( + MetadataName.RetryAfter, out TimeSpan retryAfter)) + { + response.Headers.Add( + "Retry-After", + ((int)retryAfter.TotalSeconds).ToString( + NumberFormatInfo.InvariantInfo)); + } + + return response; + } + + async ValueTask IAsyncDisposable.DisposeAsync() + { + await _rateLimiter.DisposeAsync().ConfigureAwait(false); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } +} diff --git a/docs/core/extensions/snippets/ratelimit/http/GlobalUsings.cs b/docs/core/extensions/snippets/ratelimit/http/GlobalUsings.cs new file mode 100644 index 0000000000000..86a88a70c7c2a --- /dev/null +++ b/docs/core/extensions/snippets/ratelimit/http/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using System.Globalization; +global using System.Net; +global using System.Threading.RateLimiting; diff --git a/docs/core/extensions/snippets/ratelimit/http/Program.cs b/docs/core/extensions/snippets/ratelimit/http/Program.cs new file mode 100644 index 0000000000000..b5d477c2e50d7 --- /dev/null +++ b/docs/core/extensions/snippets/ratelimit/http/Program.cs @@ -0,0 +1,39 @@ +var options = new TokenBucketRateLimiterOptions( + tokenLimit: 3, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: 1, + replenishmentPeriod: TimeSpan.FromMilliseconds(1), + tokensPerPeriod: 1, + autoReplenishment: true); + +// Create an HTTP client with the client-side rate limited handler. +using HttpClient client = new( + handler: new ClientSideRateLimitedHandler( + limiter: new TokenBucketRateLimiter(options))); + +// Create 100 urls with a unique query string. +var oneHundredUrls = Enumerable.Range(0, 100).Select( + i => $"https://example.com?iteration={i:0#}"); + +// Flood the HTTP client with requests. +var floodOneThroughFortyNineTask = Parallel.ForEachAsync( + source: oneHundredUrls.Take(0..49), + body: (url, cancellationToken) => GetAsync(client, url, cancellationToken)); + +var floodFiftyThroughOneHundredTask = Parallel.ForEachAsync( + source: oneHundredUrls.Take(^50..), + body: (url, cancellationToken) => GetAsync(client, url, cancellationToken)); + +await Task.WhenAll( + floodOneThroughFortyNineTask, + floodFiftyThroughOneHundredTask); + +static async ValueTask GetAsync( + HttpClient client, string url, CancellationToken cancellationToken) +{ + using var response = + await client.GetAsync(url, cancellationToken); + + Console.WriteLine( + $"URL: {url}, HTTP status code: {response.StatusCode} ({(int)response.StatusCode})"); +} diff --git a/docs/core/extensions/snippets/ratelimit/http/http.csproj b/docs/core/extensions/snippets/ratelimit/http/http.csproj new file mode 100644 index 0000000000000..d36b4140a3188 --- /dev/null +++ b/docs/core/extensions/snippets/ratelimit/http/http.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + From 3dc25644ece1fa053f2d8303bfe4d622625cca34 Mon Sep 17 00:00:00 2001 From: David Pine Date: Fri, 9 Sep 2022 14:27:59 -0500 Subject: [PATCH 02/13] Corrected path --- docs/core/extensions/http-ratelimiter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index 85a4cabc2a699..2030a531479d1 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -18,7 +18,7 @@ Rate limiting is the concept of limiting how much a resource can be accessed. Fo To control the flow of requests, you implement a custom subclass. This is a type of that allows you to intercept and handle requests before they are sent to the server. You can also intercept and handle responses before they are returned to the caller. In this example, you'll implement a custom `DelegatingHandler` subclass that limits the number of requests that can be sent to a single resource. Consider the following custom `ClientSideRateLimitedHandler` class: -:::code language="csharp" source="snippets/ratelimit/http/http/ClientSideRateLimitedHandler.cs"::: +:::code language="csharp" source="snippets/ratelimit/http/ClientSideRateLimitedHandler.cs"::: The preceding C# code: From bf6a02093a7c406c83658e33fd3242ee5e54522e Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 14 Sep 2022 11:29:45 -0500 Subject: [PATCH 03/13] Added the article to the HTTP section of networking --- docs/fundamentals/toc.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/fundamentals/toc.yml b/docs/fundamentals/toc.yml index 55b4b00761286..32fa9566f5512 100644 --- a/docs/fundamentals/toc.yml +++ b/docs/fundamentals/toc.yml @@ -2580,6 +2580,8 @@ items: href: ../core/extensions/httpclient-factory.md - name: HTTP/3 with .NET href: ../core/extensions/httpclient-http3.md + - name: Rate limiting an HTTP handler in .NET + href: ../core/extensions/http-ratelimiter.md - name: Sockets items: - name: Sockets support From f4afe784d2bb24c66c5083c8d371e47ae3af1bb2 Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 14 Sep 2022 12:34:01 -0500 Subject: [PATCH 04/13] Add a bit more detail about the implementation --- docs/core/extensions/http-ratelimiter.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index 2030a531479d1..f3414e967fd6a 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -24,7 +24,15 @@ The preceding C# code: - Inherits the `DelegatingHandler` type. - Implements the interface. -- Defines a `RateLimiter` field. +- Defines a `RateLimiter` field that is assigned from the constructor. +- The `SendAsync` method is overridden to intercept and handle requests before they are sent to the server. +- The `DisposeAsync` method is overridden to dispose of the `RateLimiter` instance. + +Looking a bit closer at the `SendAsync` method, you'll see that it: + +- Relies on the `RateLimiter` instance to acquire a `RateLimitLease` from the `WaitAsync`. +- When the `lease.IsAcquired` property is `true`, the request is sent to the server. +- Otherwise, an is returned with a `429` status code, and if the `lease` contains a `RetryAfter` value, the `Retry-After` header is set to that value. ## See also From 8bac603bac23fb5487ae31911f7879956b113ead Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 14 Sep 2022 13:02:35 -0500 Subject: [PATCH 05/13] More updates --- docs/core/extensions/http-ratelimiter.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index f3414e967fd6a..f4f26816cf240 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -14,6 +14,8 @@ In this article, you'll learn how to create a client-side HTTP handler that rate Rate limiting is the concept of limiting how much a resource can be accessed. For example, you may know that a database your app accesses can safely handle 1,000 requests per minute, but it may not handle much more than that. You can put a rate limiter in your appl that only allows 1,000 requests every minute and rejects any more requests before they can access the database. Thus, rate limiting your database and allowing your app to handle a safe number of requests. This is a common pattern in distributed systems, where you may have multiple instances of an app running, and you want to ensure that they don't all try to access the database at the same time. There are multiple different rate-limiting algorithms to control the flow of requests. +To use rate limiting in .NET, you'll reference the [System.Threading.RateLimiting](https://www.nuget.org/packages/System.Threading.RateLimiting) NuGet package. + ## Implement a `DelegatingHandler` subclass To control the flow of requests, you implement a custom subclass. This is a type of that allows you to intercept and handle requests before they are sent to the server. You can also intercept and handle responses before they are returned to the caller. In this example, you'll implement a custom `DelegatingHandler` subclass that limits the number of requests that can be sent to a single resource. Consider the following custom `ClientSideRateLimitedHandler` class: @@ -34,6 +36,23 @@ Looking a bit closer at the `SendAsync` method, you'll see that it: - When the `lease.IsAcquired` property is `true`, the request is sent to the server. - Otherwise, an is returned with a `429` status code, and if the `lease` contains a `RetryAfter` value, the `Retry-After` header is set to that value. +## Emulate many concurrent requests + +To put this custom `DelegatingHandler` subclass to the test, you'll create a console app that emulates many concurrent requests. This `Program` class creates an with the custom `ClientSideRateLimitedHandler`: + +:::code language="csharp" source="snippets/ratelimit/http/Program.cs"::: + +In the preceding console app: + +- The `TokenBucketRateLimiterOptions` are configured with a token limit of `3`, and queue processing order of `OldestFirst`, a queue limit of `1`, and replenishment period of `1` millisecond, a tokens per period value of `1`, and an auto-replenish value of `true`. +- An `HttpClient` is created with the `ClientSideRateLimitedHandler` that is configured with the `TokenBucketRateLimiter`. +- To emulate 100 requests, creates 100 URLs, each with a unique query string parameter. +- Two objects are assigned from the method, splitting the URLs into two groups. +- The `HttpClient` is used to send a `GET` request to each URL, and the response is written to the console. +- waits for both tasks to complete. + +Since the `HttpClient` is configured with the `ClientSideRateLimitedHandler`, not all requests will actually make it to the server resource. + ## See also - [Announcing Rate Limiting for .NET](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet) From 97ec21e15cb90bb7663eeef2af8c8296e9714d9f Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 14 Sep 2022 13:19:34 -0500 Subject: [PATCH 06/13] Should be pretty close to done now --- docs/core/extensions/http-ratelimiter.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index f4f26816cf240..180483566e1a3 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -51,9 +51,17 @@ In the preceding console app: - The `HttpClient` is used to send a `GET` request to each URL, and the response is written to the console. - waits for both tasks to complete. -Since the `HttpClient` is configured with the `ClientSideRateLimitedHandler`, not all requests will actually make it to the server resource. +Since the `HttpClient` is configured with the `ClientSideRateLimitedHandler`, not all requests will make it to the server resource. You can test this by running the console app, and you'll see that only a fraction of the total number of requests are sent to the server, and the rest are rejected with an HTTP status code of `429`. Try altering the `options` object used to create the `TokenBucketRateLimiter` to see how the number of requests that are sent to the server changes. + +To have a better understanding of the various rate-limiting algorithms, try rewriting this code to accept a different `RateLimiter` implementation. In addition to the `TokenBucketRateLimiter` you could try: + +- `ConcurrencyLimiter` +- `FixedWindowRateLimiter` +- `PartitionedRateLimiter` +- `SlidingWindowRateLimiter` ## See also - [Announcing Rate Limiting for .NET](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet) - [Rate limiting middleware in ASP.NET Core](/aspnet/core/performance/rate-limit) +- [Azure Architecture: Rate limiting pattern](/azure/architecture/patterns/rate-limiting-pattern) From ceb709603117178829b7741fc953a5d60c4c021a Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 14 Sep 2022 13:26:31 -0500 Subject: [PATCH 07/13] More links and a summary --- docs/core/extensions/http-ratelimiter.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index 180483566e1a3..0377d1c8d05e6 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -60,8 +60,13 @@ To have a better understanding of the various rate-limiting algorithms, try rewr - `PartitionedRateLimiter` - `SlidingWindowRateLimiter` +## Summary + +In this article, you learned how to implement a custom `ClientSideRateLimitedHandler`. This pattern could be used to implement a rate-limited HTTP client for resources that you know have API limits. In this way, you're preventing your client app from making unnecessary requests to the server, and you're also preventing your app from being blocked by the server. Additionally, with the use of metadata to store retry timing values, you could also implement automatic retry logic. + ## See also - [Announcing Rate Limiting for .NET](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet) - [Rate limiting middleware in ASP.NET Core](/aspnet/core/performance/rate-limit) - [Azure Architecture: Rate limiting pattern](/azure/architecture/patterns/rate-limiting-pattern) +- [Automatic retry logic in .NET](../../architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly.md) From 1688431026b0e023885f20071fd95e97f255e79e Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 14 Sep 2022 13:30:43 -0500 Subject: [PATCH 08/13] A bit more contextual content --- docs/core/extensions/http-ratelimiter.md | 108 +++++++++++++++++- .../snippets/ratelimit/http/Program.cs | 8 +- 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index 0377d1c8d05e6..0f36b86b5f59d 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -44,7 +44,7 @@ To put this custom `DelegatingHandler` subclass to the test, you'll create a con In the preceding console app: -- The `TokenBucketRateLimiterOptions` are configured with a token limit of `3`, and queue processing order of `OldestFirst`, a queue limit of `1`, and replenishment period of `1` millisecond, a tokens per period value of `1`, and an auto-replenish value of `true`. +- The `TokenBucketRateLimiterOptions` are configured with a token limit of `8`, and queue processing order of `OldestFirst`, a queue limit of `3`, and replenishment period of `1` millisecond, a tokens per period value of `2`, and an auto-replenish value of `true`. - An `HttpClient` is created with the `ClientSideRateLimitedHandler` that is configured with the `TokenBucketRateLimiter`. - To emulate 100 requests, creates 100 URLs, each with a unique query string parameter. - Two objects are assigned from the method, splitting the URLs into two groups. @@ -53,6 +53,112 @@ In the preceding console app: Since the `HttpClient` is configured with the `ClientSideRateLimitedHandler`, not all requests will make it to the server resource. You can test this by running the console app, and you'll see that only a fraction of the total number of requests are sent to the server, and the rest are rejected with an HTTP status code of `429`. Try altering the `options` object used to create the `TokenBucketRateLimiter` to see how the number of requests that are sent to the server changes. +Consider the following example output: + +```Output +URL: https://example.com?iteration=06, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=60, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=55, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=59, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=57, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=11, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=63, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=13, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=62, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=65, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=64, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=67, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=14, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=68, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=16, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=69, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=70, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=71, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=17, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=18, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=72, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=73, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=74, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=19, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=75, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=76, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=79, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=77, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=21, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=78, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=81, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=22, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=80, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=20, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=82, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=83, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=23, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=84, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=24, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=85, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=86, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=25, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=87, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=26, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=88, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=89, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=27, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=90, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=28, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=91, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=94, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=29, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=93, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=96, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=92, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=95, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=31, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=30, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=97, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=98, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=99, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=32, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=33, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=34, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=35, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=36, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=37, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=38, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=39, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=40, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=41, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=42, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=43, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=44, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=45, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=46, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=47, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=48, HTTP status code: TooManyRequests (429) +URL: https://example.com?iteration=15, HTTP status code: OK (200) +URL: https://example.com?iteration=04, HTTP status code: OK (200) +URL: https://example.com?iteration=54, HTTP status code: OK (200) +URL: https://example.com?iteration=08, HTTP status code: OK (200) +URL: https://example.com?iteration=00, HTTP status code: OK (200) +URL: https://example.com?iteration=51, HTTP status code: OK (200) +URL: https://example.com?iteration=10, HTTP status code: OK (200) +URL: https://example.com?iteration=66, HTTP status code: OK (200) +URL: https://example.com?iteration=56, HTTP status code: OK (200) +URL: https://example.com?iteration=52, HTTP status code: OK (200) +URL: https://example.com?iteration=12, HTTP status code: OK (200) +URL: https://example.com?iteration=53, HTTP status code: OK (200) +URL: https://example.com?iteration=07, HTTP status code: OK (200) +URL: https://example.com?iteration=02, HTTP status code: OK (200) +URL: https://example.com?iteration=01, HTTP status code: OK (200) +URL: https://example.com?iteration=61, HTTP status code: OK (200) +URL: https://example.com?iteration=05, HTTP status code: OK (200) +URL: https://example.com?iteration=09, HTTP status code: OK (200) +URL: https://example.com?iteration=03, HTTP status code: OK (200) +URL: https://example.com?iteration=58, HTTP status code: OK (200) +URL: https://example.com?iteration=50, HTTP status code: OK (200) +``` + +You'll notice that the first logged entries are always the immediately returned 429s, and the last entries are always the 200s. This is because the rate limit is encountered client-side, and avoids making an HTTP call to a server. This is a good thing because it means that the server is not being flooded with requests, and it also means that the rate limit is enforced consistently across all clients. + To have a better understanding of the various rate-limiting algorithms, try rewriting this code to accept a different `RateLimiter` implementation. In addition to the `TokenBucketRateLimiter` you could try: - `ConcurrencyLimiter` diff --git a/docs/core/extensions/snippets/ratelimit/http/Program.cs b/docs/core/extensions/snippets/ratelimit/http/Program.cs index b5d477c2e50d7..8e9f018938f7a 100644 --- a/docs/core/extensions/snippets/ratelimit/http/Program.cs +++ b/docs/core/extensions/snippets/ratelimit/http/Program.cs @@ -1,9 +1,9 @@ var options = new TokenBucketRateLimiterOptions( - tokenLimit: 3, + tokenLimit: 8, queueProcessingOrder: QueueProcessingOrder.OldestFirst, - queueLimit: 1, + queueLimit: 3, replenishmentPeriod: TimeSpan.FromMilliseconds(1), - tokensPerPeriod: 1, + tokensPerPeriod: 2, autoReplenishment: true); // Create an HTTP client with the client-side rate limited handler. @@ -11,6 +11,8 @@ handler: new ClientSideRateLimitedHandler( limiter: new TokenBucketRateLimiter(options))); + + // Create 100 urls with a unique query string. var oneHundredUrls = Enumerable.Range(0, 100).Select( i => $"https://example.com?iteration={i:0#}"); From 784290c58bd92e8634b6b2c9a8d6199e3a9095f9 Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 14 Sep 2022 13:46:29 -0500 Subject: [PATCH 09/13] Added override for Dispose --- .../ratelimit/http/ClientSideRateLimitedHandler.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/core/extensions/snippets/ratelimit/http/ClientSideRateLimitedHandler.cs b/docs/core/extensions/snippets/ratelimit/http/ClientSideRateLimitedHandler.cs index eb776f0c6f5e1..a06603f7c5baf 100644 --- a/docs/core/extensions/snippets/ratelimit/http/ClientSideRateLimitedHandler.cs +++ b/docs/core/extensions/snippets/ratelimit/http/ClientSideRateLimitedHandler.cs @@ -37,4 +37,14 @@ async ValueTask IAsyncDisposable.DisposeAsync() Dispose(disposing: false); GC.SuppressFinalize(this); } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _rateLimiter.Dispose(); + } + } } From 0b6fd5c82c0a2698dd692437d41564beab7e9632 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 15 Sep 2022 07:40:03 -0500 Subject: [PATCH 10/13] Added alert/include for preview bits --- docs/core/extensions/http-ratelimiter.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index 0f36b86b5f59d..f20d36df1add6 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -3,11 +3,13 @@ title: Rate limiting an HTTP handler in .NET description: Learn how to create a client-side HTTP handler that limits the number of requests. author: IEvangelist ms.author: dapine -ms.date: 09/09/2022 +ms.date: 09/15/2022 --- # Rate limiting an HTTP handler in .NET +[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] + In this article, you'll learn how to create a client-side HTTP handler that rate limits the number of requests it sends. In this example, you'll see an that accesses the `"www.example.com"` resource. Resources are consumed by apps that rely on them, and when an app makes too many requests to a single resource this can lead to resource contention. Resource contention is when a resource is consumed by too many apps, and the resource is unable to serve all of the apps that are requesting it. This can lead to a poor user experience, and in some cases, it can even lead to a denial of service (DoS) attack. For more information on DoS, see [OWASP: Denial of Service](https://owasp.org/www-community/attacks/Denial_of_Service). ## What is rate limiting? From 83eb10016e63f7197e2633e6e6b4657056ff5322 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 15 Sep 2022 07:53:33 -0500 Subject: [PATCH 11/13] Added a new include as I needed something generic to .NET preview bits. --- docs/core/extensions/http-ratelimiter.md | 2 +- includes/preview-content.md | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 includes/preview-content.md diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index f20d36df1add6..2ab73a9dc73a8 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -8,7 +8,7 @@ ms.date: 09/15/2022 # Rate limiting an HTTP handler in .NET -[!INCLUDE [scl-preview](../../../includes/scl-preview.md)] +[!INCLUDE [scl-preview](../../../includes/preview-content.md)] In this article, you'll learn how to create a client-side HTTP handler that rate limits the number of requests it sends. In this example, you'll see an that accesses the `"www.example.com"` resource. Resources are consumed by apps that rely on them, and when an app makes too many requests to a single resource this can lead to resource contention. Resource contention is when a resource is consumed by too many apps, and the resource is unable to serve all of the apps that are requesting it. This can lead to a poor user experience, and in some cases, it can even lead to a denial of service (DoS) attack. For more information on DoS, see [OWASP: Denial of Service](https://owasp.org/www-community/attacks/Denial_of_Service). diff --git a/includes/preview-content.md b/includes/preview-content.md new file mode 100644 index 0000000000000..439d1b0a2d690 --- /dev/null +++ b/includes/preview-content.md @@ -0,0 +1,9 @@ +--- +author: IEvangelist +ms.author: dapine +ms.date: 09/15/2022 +ms.topic: include +--- + +> [!IMPORTANT] +> This content relies on NuGet packages that are currently in PREVIEW. Some information relates to prerelease product that may be substantially modified before it's released. Microsoft makes no warranties, express or implied, with respect to the information provided here. From 0c8f381813612cac035e08128fde74bb777796bf Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 15 Sep 2022 08:01:08 -0500 Subject: [PATCH 12/13] Apply suggestions from code review Co-authored-by: Bill Wagner --- docs/core/extensions/http-ratelimiter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index 2ab73a9dc73a8..5362229f7e992 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -14,7 +14,7 @@ In this article, you'll learn how to create a client-side HTTP handler that rate ## What is rate limiting? -Rate limiting is the concept of limiting how much a resource can be accessed. For example, you may know that a database your app accesses can safely handle 1,000 requests per minute, but it may not handle much more than that. You can put a rate limiter in your appl that only allows 1,000 requests every minute and rejects any more requests before they can access the database. Thus, rate limiting your database and allowing your app to handle a safe number of requests. This is a common pattern in distributed systems, where you may have multiple instances of an app running, and you want to ensure that they don't all try to access the database at the same time. There are multiple different rate-limiting algorithms to control the flow of requests. +Rate limiting is the concept of limiting how much a resource can be accessed. For example, you may know that a database your app accesses can safely handle 1,000 requests per minute, but it may not handle much more than that. You can put a rate limiter in your app that only allows 1,000 requests every minute and rejects any more requests before they can access the database. Thus, rate limiting your database and allowing your app to handle a safe number of requests. This is a common pattern in distributed systems, where you may have multiple instances of an app running, and you want to ensure that they don't all try to access the database at the same time. There are multiple different rate-limiting algorithms to control the flow of requests. To use rate limiting in .NET, you'll reference the [System.Threading.RateLimiting](https://www.nuget.org/packages/System.Threading.RateLimiting) NuGet package. From 9c1b9043b861e2c3add262f88f8dcb4054e1f0e5 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 15 Sep 2022 08:06:35 -0500 Subject: [PATCH 13/13] Added a bit more about what to look at with the example app --- docs/core/extensions/http-ratelimiter.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/core/extensions/http-ratelimiter.md b/docs/core/extensions/http-ratelimiter.md index 5362229f7e992..f2efdf34a95c5 100644 --- a/docs/core/extensions/http-ratelimiter.md +++ b/docs/core/extensions/http-ratelimiter.md @@ -30,7 +30,7 @@ The preceding C# code: - Implements the interface. - Defines a `RateLimiter` field that is assigned from the constructor. - The `SendAsync` method is overridden to intercept and handle requests before they are sent to the server. -- The `DisposeAsync` method is overridden to dispose of the `RateLimiter` instance. +- The method is overridden to dispose of the `RateLimiter` instance. Looking a bit closer at the `SendAsync` method, you'll see that it: @@ -161,6 +161,8 @@ URL: https://example.com?iteration=50, HTTP status code: OK (200) You'll notice that the first logged entries are always the immediately returned 429s, and the last entries are always the 200s. This is because the rate limit is encountered client-side, and avoids making an HTTP call to a server. This is a good thing because it means that the server is not being flooded with requests, and it also means that the rate limit is enforced consistently across all clients. +You should also note that each URL's query string is unique, examine the `iteration` parameter to see that it is incremented by one for each request. This helps to illustrate that the 429 responses aren't from the first requests, but rather from the requests that are made after the rate limit is reached. The 200 responses finish later but were made earlier before the limit was reached. + To have a better understanding of the various rate-limiting algorithms, try rewriting this code to accept a different `RateLimiter` implementation. In addition to the `TokenBucketRateLimiter` you could try: - `ConcurrencyLimiter`