Reputation: 461
I wrote an API rate limiter to use with Last.fm's API.
Last.fm's Tos states that I cannot make more than 5 requests per originating IP address per second, averaged over a 5 minute period.
Here is the class I wrote:
public class RateLimiter
{
private static readonly List<DateTime> _requests = new List<DateTime>();
private const double _perMillisecond = 1000.1;
private const int _rateLimit = 5;
private const int _rateLimitCooldownMs = 500;
public static void CheckLimiter()
{
_requests.Add(DateTime.Now);
var requestsDuringRateLimit = _requests.Where(w => (DateTime.Now - w).TotalMilliseconds < _perMillisecond).ToArray();
if (requestsDuringRateLimit.Count() >= _rateLimit)
{
Thread.Sleep(_rateLimitCooldownMs);
_requests.Clear();
Console.Clear();
}
}
}
The CheckLimiter
method is called before the HttpWebRequest
is initiated, is this a good way to limit API requests?
Upvotes: 4
Views: 5441
Reputation: 1006
I wrote a library RateLimiter to handle this kind of constraints. Main advantage from our proposed solution is that it is asynchroneous and cancellable. Another feature is that you can compose constraints to build complex constraint.
Sample:
var timeconstraint = TimeLimiter.GetFromMaxCountByInterval(5, TimeSpan.FromSeconds(1));
for(int i=0; i<1000; i++)
{
await timeconstraint.Perform(ConsoleIt);
}
....
private Task ConsoleIt()
{
Trace.WriteLine(string.Format("{0:MM/dd/yyy HH:mm:ss.fff}", DateTime.Now));
return Task.FromResult(0);
}
Composed:
var constraint = new CountByIntervalAwaitableConstraint(5, TimeSpan.FromSeconds(1));
//Create second constraint: one time each 100 ms
var constraint2 = new CountByIntervalAwaitableConstraint(1, TimeSpan.FromMilliseconds(100));
//Compose the two constraints
var timeconstraint = TimeLimiter.Compose(constraint, constraint2);
//Use it
for(int i=0; i<1000; i++)
{
await timeconstraint.Perform(ConsoleIt);
}
It is also available as a nuget package.
Upvotes: 2
Reputation: 3430
This is quite fine in my opinion. Except that, there is a bug in this code. This is because what if each request is done more than a second after one another? It will never go inside that if
block. Thus, some kind of memory leak because the _requests
will grow larger over time and possibly never be cleared if my scenario above always happens.
Example:
for (int i = 0; i < 100; i++)
{
RateLimiter.CheckLimiter();
Thread.Sleep(2000);
}
What you can do is to remove entries in your _requests
that are exceeding the 1 second rule like adding this line at the end of your method.
if (_requests.Count != 0)
{
//remove irrelevant/expired entries
_requests.RemoveAll(date => (DateTime.Now - date).TotalMilliseconds >= _perMillisecond);
}
Upvotes: 2