As your website/app grows, it inevitably becomes a target of malicious agents—bots and spammers. These bots often try to scrape all your data, brute-force logins, or use fake credit cards for purchases, which can lead to revenue losses in the future.

A simple and yet, effective strategy to mitigate malicious agents is rate limiting. In simple words, rate limiting puts a cap on the number of requests a user can make in a specific time period. The cap is set to filter out the bots without blocking genuine users.

Suppose there's a login page. There's no legitimate reason why a single user would make, say, twenty login requests within a period of ten minutes. So, if that threshold is reached for particular user, we can trigger the rate limiter for them and block additional requests until the time period expires.

To achieve this, sites need a way to uniquely identify the user. If your app is behind a login wall, this task is relatively easy. But what if you don't have a login wall? Take, for example, a publicly accessible news website.

In cases like these, the simplest way to sort out malicious traffic is to use the IP address for authentication. It's true that IP addresses can be faked via a proxy or VPN. That's one reason Distributed DoS attacks are difficult to mitigate.

However, at IPinfo, we found that most abusers don't have these sophisticated tools at their disposal. They aren't using a long list of proxies or VPNs. Rather they are running simple scripts from a single host or a computer. An IP-based rate limiter is more than enough to deter abuse.

Rate Limiting Strategies

There are several strategies companies can use. Let's look at the pros and cons of each one.

Token bucket

On the most basic level, the token bucket algorithm withdraws a token with every request. Then tokens are added to the "bucket" in regular increments. Essentially, these algorithms rely more on the system's capacity to service requests rather than enforce a hard limit. Then when there are no tokens left in the bucket, the system will wait to process the requests until more tokens are added by the system.

The benefit of token buckets is that they allow companies to monitor the total capacity of their servers. The downside to this strategy is that if there's a burst of traffic, users may not be able to access the pages, services, or content they need.

Leaky bucket

The advantage here is that leaky buckets smooth out how sites process requests. But just like token buckets, traffic bursts can easily consume the limited capacity of requests. Here's how this works.

Leaky buckets differ from token bucket algorithm specifically in how requests are processed. When the bucket reaches its limit of requests, any excess requests are discarded. Basically, buckets have a fixed capacity. When requests are added to the bucket, it's first come first serve. Any overage requests are "leaked" from the bucket and discarded.

Fixed window

In a fixed window algorithm, the system tracks the requests made in a window of fixed size (specifically, in seconds). If the requests exceed the threshold, then the system discards these requests and the user has to wait for the window to reset.

The advantage here is that old requests aren't stuck forever to be processed. However, if there's a burst of requests near the edge of the window, then the system would be processing more requests than capacity.

For example, let's say the system has processed 10 requests in a one-minute window and the capacity is 20 requests per minute. Then when the window is almost reset, another burst of 20 requests arrives. This means that the system is now processing 30 requests per minute... 10 requests beyond the normal limit.

Sliding window

To resolve the problem of boundary conditions, companies can use a sliding window algorithm. Instead of keeping the window fixed, systems timestamp every request and then check if the requests made in the past n seconds (a.k.a. the window size) have exceeded the threshold. If they have, this algorithm will discard any overages.

This approach, however, can be more expensive. But the benefit is that spikes in traffic won't negatively affect the site.

Building a Rate Limiter

Of these four options, the sliding window algorithm is the best. But, as mentioned, it also has a higher price tag when it comes to implementation. Unless you have very specific needs or are working on a very large system, you might not need to smooth out bursts to this extent.

In our case at IPinfo, a fixed window algorithm is more than enough to solve our problems. So that's what we'll use for the rate limiter in the following example.

We'll also be using Redis, which has a library in nearly every language. Redis is fast, simple, and works elegantly with distributed systems.

Let's suppose we have to create a rate limit such as this: how many times a user can use a contact form. The limit we're going to set is five requests every hour.

Our code is going to be in Node + Express.js, but it's also generic enough to be used anywhere.

Step 1: Create a Middleware

In express.js, every request goes through functions called middlewares. Each middleware can either do some processing, send a response, or just call the next middleware in the stack. Most application frameworks, like Django and Rails, offer a similar feature.

We can create a rate-limit middleware that checks if the user has made too many requests. If yes, it will show an error message. Otherwise, it will pass on the request.

// ratelimit.js

const asyncRedis = require("async-redis");
const client = asyncRedis.createClient();

REQUESTS_LIMIT = 5;
TTL = 60 * 60; // 3600 seconds = an hour

module.exports = async (req, res, next) => {
	const key = req.ip;
	const count = await client.incr(key);

  // If key is created for the first time, set expiry
	if (count === 1) {
		client.expire(key, TTL);
	}

	if (count > REQUESTS_LIMIT) {
		return res.status(429).send("Too many requests!");
	}

	next();
}

Step 2: Use The Middleware

Once the middleware is ready, we can start using it for our contact page submission route.

// app.js
const contactPage = require('./contactPage');
const rateLimiter = require('./ratelimit');

// ... express.js boilerplate

app.post('/contact', rateLimiter, contactPage);

Step 3: Make it More Generic

Perfect! It works. If you make more than five requests in five minutes, it will throw an error. However, this rate limit is only tied to one specific page. What if we want to set limits on other pages? To achieve that, let's make it more generic so that it can handle different routes just as well. We will do that by creating a function that returns a middleware.

// ratelimit.js
const asyncRedis = require("async-redis");
const client = asyncRedis.createClient();

module.exports = (prefix, requestLimit, ttl) => {
	return async (req, res, next) => {
		const key = `${prefix}${req.ip)`;
		const count = await client.incr(key);
		
		if (count === 1) {
			client.expire(key, ttl);
		}
		
		if (count > requestLimit) {
			return res.status(429).send("Too many requests!");
		}

		next();
	}
}

// app.js

// Ten attempts every hour for contact page
app.post('/contact', rateLimiter('contact:', 10, 60 * 60), contactPage);

// Five attempts every ten minutes for contact page
app.post('/login', rateLimiter('login:', 5, 10 * 60), loginPage);

Our rate limiter is now ready. It isn't foolproof, but it does the job of blocking the most common forms of abuse.

There's one more thing to keep in mind as far as website ranking. Rate limiting every user can potentially block search engine crawlers and hurt your SEO. Therefore, it makes sense to whitelist their user agent, such as Googlebot. However, since user agents can also be faked, it's often best to rely on more sophisticated checks like checking hostname. If you're using IPinfo API, this can be done by using the hostname property.

You should also ensure that genuine users aren't blocked from accessing the app further. For instance, login failures could also mean the user has forgotten their password. You should make sure that the limits you're imposing are generous enough for these instances.

Found this tutorial helpful? We'd love to hear about your experience with rate limiters, message us here or on Twitter!

IPinfo is a comprehensive IP address data and API provider with flexible pricing plans to meet your business needs. We handle billions of API requests per month, serving data like IP geolocation, VPN detection, Reverse IP, and more.