228

We're experimenting with various ways to throttle user actions in a given time period:

  • Limit question/answer posts
  • Limit edits
  • Limit feed retrievals

For the time being, we're using the Cache to simply insert a record of user activity - if that record exists if/when the user does the same activity, we throttle.

Using the Cache automatically gives us stale data cleaning and sliding activity windows of users, but how it will scale could be a problem.

What are some other ways of ensuring that requests/user actions can be effectively throttled (emphasis on stability)?

Jarrod Dixon
  • 15,727
  • 9
  • 60
  • 72
  • Are you trying to limit per user or per question? If per user, could use session, which would be a smaller set. – oglester Nov 02 '08 at 01:23
  • 1
    It's per user, but we couldn't use Session, as that requires cookies - we're limiting based on IP address currently. – Jarrod Dixon Nov 13 '08 at 02:20
  • 1
    Nowadays, consider nuget packages https://github.com/stefanprodan/MvcThrottle for MVC pages and https://github.com/stefanprodan/WebApiThrottle for web api requests – Andy Sep 05 '18 at 11:30

6 Answers6

258

Here's a generic version of what we've been using on Stack Overflow for the past year:

/// <summary>
/// Decorates any MVC route that needs to have client requests limited by time.
/// </summary>
/// <remarks>
/// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
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(ActionExecutingContext c)
    {
        var key = string.Concat(Name, "-", c.HttpContext.Request.UserHostAddress);
        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.";

            c.Result = new ContentResult { Content = Message.Replace("{n}", Seconds.ToString()) };
            // see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
            c.HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
        }
    }
}

Sample usage:

[Throttle(Name="TestThrottle", Message = "You must wait {n} seconds before accessing this url again.", Seconds = 5)]
public ActionResult TestThrottle()
{
    return Content("TestThrottle executed");
}

The ASP.NET Cache works like a champ here - by using it, you get automatic clean-up of your throttle entries. And with our growing traffic, we're not seeing that this is an issue on the server.

Feel free to give feedback on this method; when we make Stack Overflow better, you get your Ewok fix even faster :)

Cœur
  • 37,241
  • 25
  • 195
  • 267
Jarrod Dixon
  • 15,727
  • 9
  • 60
  • 72
  • 5
    quick question - you're using the c.HttpContext.Request.UserHostAddress value as part of the key. Is that value possible empty or null or all the same value? (ie, if u're using a load balancer and it's the IP of that machine .. not the real clients) Like, do proxy's or load balancers (ie an BIG IP F5) put the same data in there and you need to check for X-Forwarded-For also or something? – Pure.Krome Jun 27 '10 at 14:23
  • 7
    @Pure.Krome - yes, it could be. When retrieving the client IP, we use a helper function that checks both the `REMOTE_ADDR` and `HTTP_X_FORWARDED_FOR` server variables and sanitizes appropriately. – Jarrod Dixon Aug 13 '10 at 06:11
  • 2
    But this wouldn't work in a web farm would it? Given that how can it work with Stack Overflow? Don't you need some sort of shared cache such as with Windows Server AppFabric or memcached? – BrettRobi Apr 13 '11 at 17:19
  • 3
    @BrettRobi, I'm pretty sure they have server affinity based on the users IP address. So they will likely still be hitting the same server. – mmcdole Sep 23 '11 at 04:34
  • 2
    You may want to also add `c.HttpContext.Response.TrySkipIisCustomErrors = true;` when running IIS7 on Integrated Mode. This will make sure your custom message is not replaced by "The page was not displayed because there was a conflict." – Yosoyadri Oct 27 '12 at 21:29
  • 2
    I'm curious if you've updated this to use status code 429 instead of 409. – tvanfosson Dec 10 '12 at 18:17
  • @tvanfosson no, I hadn't even seen that before. I wonder when it will be added to the [`System.Net.HttpStatusCode`](http://msdn.microsoft.com/en-us/library/system.net.httpstatuscode(v=vs.110).aspx) enumeration? – Jarrod Dixon Dec 10 '12 at 19:00
  • 2
    Two questions: 1. What about the case with large networks behind a NAT? All requests will come from the same public IP. Have you had any such problems or needed to whitelist some IP addresses? 2. I think that an attacker can fake the X-Forwarded-For header, adding it to the request headers, and get past this throttling limit. Is that right? – Iravanchi Mar 09 '13 at 11:15
  • 1
    @Iravanchi 1. yes, this happens, and for some of our throttles, we've incorporated user identity into the key, instead of IP. 2. we rely on what our [HAProxy load balancer](https://code.google.com/p/haproxy-docs/wiki/forwardfor) gives us for the X-Forwarded-For, and only take the last occurrence. – Jarrod Dixon Mar 09 '13 at 21:34
  • Does this code have a race condition where two threads trying to add the same key could conflict? E.g. 1) Thread 1 checks HttpRuntime.Cache[key] == null, result is true 2) Thread 2 checks HttpRuntime.Cache[key] == null, result is true 3) Thread 1 calls Cache.Add, succeeds 4) Thread 2 calls Cache.Add, fails due to the key already existing. – RyanY May 02 '13 at 19:13
  • @Phage yes, a race condition exists, but we don't really care. The method was created to guard expensive database calls, so an extra execution by the same client is fine - it's just simplistic flood protection. – Jarrod Dixon May 03 '13 at 19:58
  • 1
    you can use 429 this way Enum.TryParse("429", out tooManyRequestStatusCode) – Trent Ahrens Aug 07 '13 at 20:29
  • just one silly unrelated doubt. why not replace `{n}` with `{0]` and use `String.Format(Message, Seconds);`. any performance issue? You guys are the SO. Thats why this doubt – naveen Sep 03 '13 at 04:10
  • @JarrodDixon - we are using a throttle that is very similar to this one (we copied yours and added to it). We have a problem where certin Redirects in our various actions will actually trip the throttle because it thinks the user is making a second request. I was wondering if you had the same problem and how you solved it. So far, I have been unsuccessful finding a way to detect that the ActionExecutingContext came from an app redirect vs. a user request. Besides that issue, thanks for sharing your throttle, works great. – SLoret Apr 23 '14 at 17:21
  • 5
    For those of you who care and have read this far down in the comment stream...we ended up writting our own redirects that clear out the throttle cache key before redirecting. This way all redirects pass through the code to remove the key and none of them trigger the Throttle attribute. – SLoret Apr 25 '14 at 17:45
  • 5
    If you are looking for the Web API version of this, check here: http://stackoverflow.com/questions/20817300/how-to-throttle-requests-in-a-webapi-controller – Papa Burgundy Jul 25 '14 at 17:35
  • Hi @JarrodDixon I have a doubt. I am using session variables that force concurrent requests from a user to be queued, If a request is queued for say 1.1 sec and my time out was 1 sec, then will that request be processed or error message will be returned? –  Sep 23 '16 at 12:32
  • I have a doubt , What if does `DOS` attack hits the web server, then how come this code level protection will help ? Doesn't we need solution on web server or machine level ? – Shaiju T Dec 08 '17 at 09:46
  • How can i instead of showing a message on a blank page stay on the same page but just don't allow the multiple requests? – Lucho Gizdov Jun 11 '21 at 11:28
  • Great answer. I believe it would be more accurate to return HTTP status code 429 (too many requests) instead of `HttpStatusCode.Conflict`... even though `HttpStatusCode` has not defined an `enum` for this code. – Hooman Bahreini May 19 '23 at 04:17
70

Microsoft has a new extension for IIS 7 called Dynamic IP Restrictions Extension for IIS 7.0 - Beta.

"The Dynamic IP Restrictions for IIS 7.0 is a module that provides protection against denial of service and brute force attacks on web server and web sites. Such protection is provided by temporarily blocking IP addresses of the HTTP clients who make unusually high number of concurrent requests or who make large number of requests over small period of time." http://learn.iis.net/page.aspx/548/using-dynamic-ip-restrictions/

Example:

If you set the criteria to block after X requests in Y milliseconds or X concurrent connections in Y milliseconds the IP address will be blocked for Y milliseconds then requests will be permitted again.

Amicable
  • 3,115
  • 3
  • 49
  • 77
notandy
  • 3,330
  • 1
  • 27
  • 35
  • 1
    Do you know if it caused any problems with crawlers like the Googlebot? – Helephant Aug 23 '13 at 13:17
  • @Helephant http://webmasters.stackexchange.com/questions/26553/iis-dynamic-ip-restrictions-module-and-googlebot – Anirudha Gupta Dec 28 '13 at 17:48
  • 1
    It's now released and bundled with IIS as of version 8 - http://www.iis.net/learn/get-started/whats-new-in-iis-8/iis-80-dynamic-ip-address-restrictions – Matthew Steeples Nov 16 '15 at 12:13
  • I would love to use this, but it does NOT allow you to throttle by ``. It's every request for the app or none. – Kasey Speakman Apr 25 '17 at 22:18
  • Doesn't seem to be useful if your web servers are behind a load balancer, as all of the traffic will appear to be from the same IP address. Unless I am missing something obvious... – Dscoduc Sep 30 '19 at 21:35
11

We use the technique borrowed from this URL http://www.codeproject.com/KB/aspnet/10ASPNetPerformance.aspx, not for throttling, but for a poor man's Denial Of Service (D.O.S). This is also cache-based, and may be similar to what you are doing. Are you throttling to prevent D.O.S. attacks? Routers can certainly be used to reduce D.O.S; do you think a router could handle the throttling you need?

Rob Kraft
  • 1,122
  • 1
  • 11
  • 18
3

It took me some time to work out an equivalent for .NET 5+ (formerly .NET Core), so here's a starting point.

The old way of caching has gone and been replaced by Microsoft.Extensions.Caching.Memory with IMemoryCache.

I separated it out a bit more, so here's what you need...

The Cache Management Class

I've added the whole thing here, so you can see the using statements.

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using System;
using System.Threading;

namespace MyWebApplication
{
    public interface IThrottleCache
    {
        bool AddToCache(string key, int expriryTimeInSeconds);

        bool AddToCache<T>(string key, T value, int expriryTimeInSeconds);

        T GetFromCache<T>(string key);

        bool IsInCache(string key);
    }

    /// <summary>
    /// A caching class, based on the docs
    /// https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-6.0
    /// Uses the recommended library "Microsoft.Extensions.Caching.Memory"
    /// </summary>
    public class ThrottleCache : IThrottleCache
    {
        private IMemoryCache _memoryCache;

        public ThrottleCache(IMemoryCache memoryCache)
        {
            _memoryCache = memoryCache;
        }


        public bool AddToCache(string key, int expriryTimeInSeconds)
        {
            bool isSuccess = false; // Only a success if a new value gets added.

            if (!IsInCache(key))
            {
                var cancellationTokenSource = new CancellationTokenSource(
                                                     TimeSpan.FromSeconds(expriryTimeInSeconds));

                var cacheEntryOptions = new MemoryCacheEntryOptions()
                    .SetSize(1)
                    .AddExpirationToken(
                        new CancellationChangeToken(cancellationTokenSource.Token));

                _memoryCache.Set(key, DateTime.Now, cacheEntryOptions);

                isSuccess = true;
            }

            return isSuccess;
        }


        public bool AddToCache<T>(string key, T value, int expriryTimeInSeconds)
        {
            bool isSuccess = false;

            if (!IsInCache(key))
            {
                var cancellationTokenSource = new CancellationTokenSource(
                                                     TimeSpan.FromSeconds(expriryTimeInSeconds));

                var cacheEntryOptions = new MemoryCacheEntryOptions()
                    .SetAbsoluteExpiration(DateTimeOffset.Now.AddSeconds(expriryTimeInSeconds))
                    .SetSize(1)
                    .AddExpirationToken(
                        new CancellationChangeToken(cancellationTokenSource.Token));

                _memoryCache.Set<T>(key, value, cacheEntryOptions);

                isSuccess = true;
            }

            return isSuccess;
        }


        public T GetFromCache<T>(string key)
        {
            return _memoryCache.Get<T>(key);
        }


        public bool IsInCache(string key)
        {
            var item = _memoryCache.Get(key);

            return item != null;
        }


    }
}

The attribute itself

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Net;

namespace MyWebApplication
{
    /// <summary>
    /// Decorates any MVC route that needs to have client requests limited by time.
    /// Based on how they throttle at stack overflow (updated for .NET5+)
    /// https://stackoverflow.com/questions/33969/best-way-to-implement-request-throttling-in-asp-net-mvc/1318059#1318059
    /// </summary>
    /// <remarks>
    /// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
    /// </remarks>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class ThrottleByIPAddressAttribute : ActionFilterAttribute
    {
        /// <summary>
        /// The caching class (which will be instantiated as a singleton)
        /// </summary>
        private IThrottleCache _throttleCache;


        /// <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; } = "You may only perform this action every {n} seconds.";

        public override void OnActionExecuting(ActionExecutingContext c)
        {
            if(_throttleCache == null)
            {
                var cache = c.HttpContext.RequestServices.GetService(typeof(IThrottleCache));
                _throttleCache = (IThrottleCache)cache;
            }
            
            var key = string.Concat(Name, "-", c.HttpContext.Request.HttpContext.Connection.RemoteIpAddress);

            var allowExecute = _throttleCache.AddToCache(key, Seconds);


            if (!allowExecute)
            {
                if (String.IsNullOrEmpty(Message))
                    Message = "You may only perform this action every {n} seconds.";

                c.Result = new ContentResult { Content = Message.Replace("{n}", Seconds.ToString()) };
                // see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
                c.HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
            }
        }
    }
}

Startup.cs or Program.cs - Register the services with DI

This example uses Startup.cs/ConfigureServices - Put the code somewhere after AddControllersWithViews).

For a project created in .NET6+ I think you'd add the equivalent between builder.Services.AddRazorPages(); and var app = builder.Build(); in program.cs. services would be builder.Services.

If you don't get the placement of this code right, the cache will be empty every time you check it.

// The cache for throttling must be a singleton and requires IMemoryCache to be set up.
// Place it after AddControllersWithViews or AddRazorPages as they build a cache themselves

// Need this for IThrottleCache to work.
services.AddMemoryCache(_ => new MemoryCacheOptions
{
    SizeLimit = 1024, /* TODO: CHECK THIS IS THIS THE RIGHT SIZE FOR YOU! */
    CompactionPercentage = .3,
    ExpirationScanFrequency = TimeSpan.FromSeconds(30),
});
services.AddSingleton<IThrottleCache, ThrottleCache>();

Example Usage

[HttpGet, Route("GetTest")]
[ThrottleByIPAddress(Name = "MyControllerGetTest", Seconds = 5)]
public async Task<ActionResult<string>> GetTest()
{
    return "Hello world";
}

To help understand caching in .NET 5+, I've also made a caching console demo.

JsAndDotNet
  • 16,260
  • 18
  • 100
  • 123
1

Since the highly voted answers to this question are too old, I am sharing the latest solution which worked for me.

I tried using the Dynamic IP restrictions as given in an answer on this page but when I tried to use that extension, I found that this extension has been discontinued by Microsoft and on the download page they have clearly written the below message.

Microsoft has discontinued the Dynamic IP Restrictions extension and this download is no longer available.

So I researched further and found that the Dynamic IP Restrictions is now by default included in IIS 8.0 and above. The below information is fetched from the Microsoft Dynamic IP Restrictions page.

In IIS 8.0, Microsoft has expanded the built-in functionality to include several new features:

  • Dynamic IP address filtering, which allows administrators to configure their server to block access for IP addresses that exceed the specified number of requests.
  • The IP address filtering features now allow administrators to specify the behavior when IIS blocks an IP address, so requests from malicious clients can be aborted by the server instead of returning HTTP 403.6 responses to the client.
  • IP filtering now feature a proxy mode, which allows IP addresses to be blocked not only by the client IP that is seen by IIS but also by the values that are received in the x-forwarded-for HTTP header

For step by step instructions to implement Dynamic IP Restrictions, please visit the below link:

https://learn.microsoft.com/en-us/iis/get-started/whats-new-in-iis-8/iis-80-dynamic-ip-address-restrictions

I hope it helps someone stuck in a similar problem.

prem
  • 3,348
  • 1
  • 25
  • 57
0

Created ThrottlingTroll - my take on throttling/rate limiting in ASP.NET Core.

It is similar to Stefan Prodan's AspNetCoreRateLimit and ASP.NET 7's Rate Limiting Middleware, but has advantages:

  • Both ingress and egress throttling (egress means that your specially configured HttpClient won't make more than N requests per second and will instead produce 429 status code by itself).
  • Distributed rate counter stores (including, but not limited to Redis).
  • Dynamic (re)configuration - allows to adjust limits without restarting the service.
  • Propagating 429 statuses from egress to ingress.

Check out more in the repo.

scale_tone
  • 232
  • 2
  • 11