How to throttle requests in a Web Api?
The proposed solution is not accurate. There are at least 5 reasons for it.
- The cache does not provide interlocking control between different threads, therefore multiple requests can be process at the same time introducing extra calls skipping through the throttle.
- The Filter is being processed 'too late in the game' within web API pipeline, so lots of resources are being spent before you decide that request should not be processed. The DelegatingHandler should be used because it can be set to run at the beginning of the Web API pipeline and cutting off the request prior doing any additional work.
- The Http cache itself is dependency that might not be available with new runtimes, like self-hosted options. It is best to avoid this dependency.
- Cache in the above example does not guarantee its survival between the calls as it might be removed due to memory pressure, especially being low priority.
- Although it is not too bad issue, setting response status to 'conflict' does not seem to be the best option. It is better to use '429-too many requests' instead.
There are many more issues and hidden obstacles to solve while implementing the throttling. There are free open source options available. I recommend to look at https://throttlewebapi.codeplex.com/, for example.
WebApiThrottle is quite the champ now in this area.
It's super easy to integrate. Just add the following to App_Start\WebApiConfig.cs
:
config.MessageHandlers.Add(new ThrottlingHandler()
{
// Generic rate limit applied to ALL APIs
Policy = new ThrottlePolicy(perSecond: 1, perMinute: 20, perHour: 200)
{
IpThrottling = true,
ClientThrottling = true,
EndpointThrottling = true,
EndpointRules = new Dictionary<string, RateLimits>
{
//Fine tune throttling per specific API here
{ "api/search", new RateLimits { PerSecond = 10, PerMinute = 100, PerHour = 1000 } }
}
},
Repository = new CacheRepository()
});
It's available as a nuget too with the same name.
You seem to be confusing action filters for an ASP.NET MVC controller and action filters for an ASP.NET Web API controller. Those are 2 completely different classes:
- For ASP.NET MVC:
System.Web.Mvc.ActionFilterAttribute
-> that's what you got from the link - For ASP.NET Web API:
System.Web.Http.Filters.ActionFilterAttribute
-> that's what you need to implement
It appears that what you have shown is a Web API controller action (one that is declared inside a controller deriving from ApiController
). So if you want to apply custom filters to it, they must derive from System.Web.Http.Filters.ActionFilterAttribute
.
So let's go ahead and adapt the code for Web API:
public class ThrottleAttribute : ActionFilterAttribute
{
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// A text message that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
/// </summary>
public string Message { get; set; }
public override void OnActionExecuting(HttpActionContext actionContext)
{
var key = string.Concat(Name, "-", GetClientIp(actionContext.Request));
var allowExecute = false;
if (HttpRuntime.Cache[key] == null)
{
HttpRuntime.Cache.Add(key,
true, // is this the smallest data we can have?
null, // no dependencies
DateTime.Now.AddSeconds(Seconds), // absolute expiration
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null); // no callback
allowExecute = true;
}
if (!allowExecute)
{
if (string.IsNullOrEmpty(Message))
{
Message = "You may only perform this action every {n} seconds.";
}
actionContext.Response = actionContext.Request.CreateResponse(
HttpStatusCode.Conflict,
Message.Replace("{n}", Seconds.ToString())
);
}
}
}
where the GetClientIp
method comes from this post
.
Now you can use this attribute on your Web API controller action.