Skip to content

Rate Limiting a Web Application

In this blog post I’ll explain how to add rate limiting to your web application. Rate limiting is used to frustrate abuse of your application by limiting access. The same technique can be used to limit access to your API server by recording the user’s ID instead of IP Address.

First, we add a class to our web project and give it the name RateLimiter.

This class needs to be a singleton so only one object can be created from the class. To do this we add a constructor method and make it private.

Next, we add a Shared Property called mInstance which is a RateLimiter. This property is private.

Now, we add a Shared Method called GetInstance which returns a RateLimiter and is public.

In this method we place the following code:

If mInstance Is Nil Then
  mInstance = New RateLimiter
End If Return mInstance

As you can see if mInstance is Nil (hasn’t been instantiated yet) we create the object and return it.

Now, add a property which we’ll call mDB which is a SQLiteDatabase and is private.

Now, add code to the constructor method we created earlier:

mDB = New SQLiteDatabase
mDB.Connect() ' No database file = In-memory database

mDB.ExecuteSQL("CREATE TABLE Requests (id INTEGER PRIMARY KEY AUTOINCREMENT, IPAddress TEXT, RequestTime TEXT);")

Here we instantiate the mDB property, connect to it and create a Table to keep track of requests.

Next, I’m going to add two constants to the class. These are kRateLimit with a value of 2 – this is the maximum number of connections from that IP Address and kTimeLimit with a value of 1 – this is the period of time in minutes that the connections can be successfully created in.

These constants are created to provide an easy point for later tweaking the settings.

Now, we create a method called ApplyLimit which records the request and returns a Boolean property depending on the number of requests from the IP address in the last kTimeLimit minutes.

The code for this method is:

mDB.ExecuteSQL("INSERT INTO Requests (IPAddress, RequestTime) VALUES (?, ?);", IPAddress, DateTime.Now.SQLDateTime)

Var Rows As RowSet = mDB.SelectSQL("SELECT COUNT(*) AS Total FROM Requests WHERE IPAddress = ?;", IPAddress)

Return Rows.Column("Total").IntegerValue > kRateLimit

Here we record the request into the database, then get a count of the number of requests for this IP Address and return true if the count is greater than kRateLimit.

This means we need to remove Requests that are older than kTimeLimit so we’ll add another method Cleanup which is private.

And the code is:

mDB.ExecuteSQL("DELETE FROM Requests WHERE RequestTime < ?;", DateTime.Now.SubtractInterval(0, 0, 0, 0, kTimeLimit).SQLDateTime)
mDB.ExecuteSQL("VACUUM")

Timer.CallLater(1000, AddressOf Cleanup)

Here we delete rows where the time < now – kTimelLimit, vacuum the database to remove the now deleted rows and lastly call this method again with a delay of 1000ms or 1 second.

This method won’t start itself, so we’ll add another line of code to the constructor:

Cleanup()

Which will start the cleanup process.

Lastly, for the class we add another public shared method to facilitate finding the IP address of the client.

The code for this method is: 

If Request.HeaderNames.IndexOf("X-Forwarded-For") > -1 Then
  Return Request.Header("X-Forwarded-For").NthField(":", 1)
ElseIf Request.HeaderNames.IndexOf("Remote-Addr") > -1 Then
  Return Request.Header("Remote-Addr").NthField(":", 1)
Else
  Return Request.RemoteAddress
End If

This function returns the IP address first looking at the header “X-Forwarded-For”, then “Remote-Addr” and if these fail, the remote address from the request itself. This allows the application to be reverse proxied behind a publishing engine such as IIS or NGINX.  The NthField(":", 1) part removes the random port that the client is connected to.

Our class is now complete, so we’ll implement this functionality by adding a HandleURL event handler to App

The code for this method is:

Var RateLimit As RateLimiter = RateLimiter.GetInstance()

If RateLimit.ApplyLimit(RateLimiter.GetIPAddress(Request)) Then
  Response.Status = 429
  Response.Write("Too many requests at " + DateTime.Now.ToString(DateTime.FormatStyles.None, DateTime.FormatStyles.Long) + ".  Try again later.")
  Return True
End If

Return False

In the first line we get a reference to the RateLimiter instance. Then we call the ApplyLimit method to record the request and find out if we need to apply the limit. If we do, then we set the status and message in the Response argument and return true to tell the framework we’ve handled the request. Otherwise, we return false to allow the request to proceed.

You can see this in action at Rate Limit Example (xojocloud.net).

Wayne Golding has been a Xojo developer since 2005 and is a Xojo MVP. He operates the IT Company Axis Direct Ltd which primarily develops applications using Xojo that integrate with Xero www.xero.com. Wayne’s hobby is robotics where he uses Xojo to build applications for his Raspberry Pi, often implementing IoT for remote control.