API Rate Limit HTTP Handler with HttpClientFactory
Tags: aspnet, aspnetcore, dotnetcore, netcore
Published: 2020-04-14 14:01:22

---
title: API Rate Limit HTTP Handler with HttpClientFactory
published: true
date: 2019-02-18 16:01:00 UTC
tags: aspnet, aspnetcore, dotnetcore, netcore
canonical_url: https://sebnilsson.com/blog/api-rate-limit-http-handler-with-httpclientfactory/
---

Most APIs have a **Rate Limit** of some sort. For example, [GitHub has a limit](https://developer.github.com/v3/#rate-limiting) of 5000 requests per hour. This can be handled, as a consumer of the API, by limiting your use by timing your requests to the API or through caching of the results.

What about when an API **limits your requests per second**? This is probably something you would want to **handle somewhere central in your code** and not spread out everywhere where you make an HTTP call to the API.

 ![Funnel](https://files.sebnilsson.com/web/images/ratelimithttpmessagehandler/water-funnel.jpg)

For me, the solution was to add a[Outgoing request middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-2.2#outgoing-request-middleware)to the setup of the[`HttpClientFactory`](https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests).

With this, I can just configure the startup services to use this `RateLimitHttpMessageHandler`-class with the `HttpClientFactory`:

```
services
    .AddHttpClient<IApi, Api>()
    .AddHttpMessageHandler(() =>
        new RateLimitHttpMessageHandler(
            limitCount: 5,
            limitTime: TimeSpan.FromSeconds(1)))
    .AddDefaultTransientHttpErrorPolicy();
```

This ensures that wherever I use the class `IApi`, through dependency injection, it will limit the calls to the API to only 5 calls per second.

The simplified version of code for the `RateLimitHttpMessageHandler`:

```
public class RateLimitHttpMessageHandler : DelegatingHandler
{
    private readonly List<DateTimeOffset> _callLog =
        new List<DateTimeOffset>();
    private readonly TimeSpan _limitTime;
    private readonly int _limitCount;

    public RateLimitHttpMessageHandler(int limitCount, TimeSpan limitTime)
    {
        _limitCount = limitCount;
        _limitTime = limitTime;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;

        lock (_callLog)
        {
            _callLog.Add(now);

            while (_callLog.Count > _limitCount)
                _callLog.RemoveAt(0);
        }

        await LimitDelay(now);

        return await base.SendAsync(request, cancellationToken);
    }

    private async Task LimitDelay(DateTimeOffset now)
    {
        if (_callLog.Count < _limitCount)
            return;

        var limit = now.Add(-_limitTime);

        var lastCall = DateTimeOffset.MinValue;
        var shouldLock = false;

        lock (_callLog)
        {
            lastCall = _callLog.FirstOrDefault();
            shouldLock = _callLog.Count(x => x >= limit) >= _limitCount;
        }

        var delayTime = shouldLock && (lastCall > DateTimeOffset.MinValue)
            ? (limit - lastCall)
            : TimeSpan.Zero;

        if (delayTime > TimeSpan.Zero)
            await Task.Delay(delayTime);
    }
}
```