Reputation: 3815
I am currently exploring solutions to limit access to an API endpoint on NodeJS by total number of requests per month.
For instance, I want the free plan users to access the /api
endpoint up to a total of 100 requests per month, and the premium plan users to have 5000 requests per month.
The naïve way to get around it is by implementing a passport middleware to get the user's plan and then keep track of the count:
app.get("/api", requireAuth, async (req, res, next) => {
try {
// Check if user ran out of requests
if (req.user.apiRequestsLeft === 0) {
res.send("You ran out of API requests!")
} else {
// Decrement the allocated requests
req.user.apiRequestsLeft--;
await req.user.save();
res.send(user)
}
} catch (err) {
next(err);
}
});
My concerns are:
Upvotes: 1
Views: 1658
Reputation: 1204
rate-limiter-flexible package helps with counters and automatically expire counters.
const opts = {
storeClient: mongoConn,
points: 5000, // Number of points
duration: 60 * 60 * 24 * 30, // Per month
};
const rateLimiterMongo = new RateLimiterMongo(opts);
const rateLimiterMiddleware = (req, res, next) => {
// req.userId should be set before this middleware
const key = req.userId ? req.userId : req.ip;
const pointsToConsume = req.userId ? 1 : 50;
rateLimiterMongo.consume(key, pointsToConsume)
.then(() => {
next();
})
.catch(_ => {
res.status(429).send('Too Many Requests');
});
};
app.use(rateLimiterMiddleware);
Note, this example is not bound to calendar month, but counts events from the first event within the next month after it. You could set custom duration with block to strictly connect counters expiry to calendar months.
This code should easily handle about 1k-2k requests per second on basic server. You could also use Redis limiter or Mongo limiter with sharding options.
Also, it provides In-memory Block strategy to avoid too much requests to MongoDB/Redis/any store.
Alternatively, play with get method from rate-limiter-flexible to decrease amount of unnecessary counters updates. get
method is much-much faster than increment.
Upvotes: 1
Reputation: 3543
Performance/Scalability issues of having to update a MongoDB document each time there's a request - is this feasible or will I hit a problem when the app grows?
Definitely. You will soon experience heavy mongoDB traffic and it will hit performance bottleneck. In my opinion, you should use a faster in-memory database like Redis to handle the situation. You can even use the Redis as the session-store
which will reduce the load on MongoDB. That way, MongoDB can be utilized for other Business queries.
Resetting the count - should this be a daily cronjob that looks at the timestamp of 'registration' of each and every user, compute if a month has passed and reset allotted requests accordingly, or is there a better way of designing something like this?
A better way would be to achieve the resetting part in the middleware itself.
Here is some code that explains my solution.
Sample design of Quota
object would be:
{
type: "FREE_USER", /** or "PREMIUM_USER" */
access_limit: 100, /** or 5000 */
exhausted_requests: 42 /** How many requests the user has made so far this month */
last_reset_timestamp: 1547796508728 /** When was the exhausted_requests set to 0 last time */
}
With that design. Your middleware that checks the quota would look something like:
const checkQuota = async (req, res, next) => {
const user = req.user;
const userQuotaStr = await redis.getAsync(user.id)
let userQuota;
/** Check if we have quota information about user */
if (userQuotaStr != null) {
/** We have previously saved quota information */
userQuota = JSON.parse(userQuotaStr);
/**
* Check if we should reset the exhausted_requests
* Assuming that all the requests are reset on the First Day of each month.
*/
if ( isStartOfMonth() ) {
/**
* It is First Day of the month. We might need to reset the `exhausted_requests`
* Check the difference between `Date.now()` and `userQuota.last_reset_timestamp`
* to determine whether we should reset or not
*/
if ( shouldResetTimeStamp(userQuota.last_reset_timestamp) ) {
userQuota.exhausted_requests = 0
userQuota.last_reset_timestamp = Date.now()
}
}
} else {
/** We do not have previously saved quota information. Prepare one */
userQuota = {
type: user.type,
access_limit: user.access_limit,
exhausted_requests: 0,
last_reset_timestamp: Date.now()
}
}
/** Incredement the counter to account the current request */
userQuota.exhausted_requests++
/** Update in database */
redis.set(user.id, JSON.stringify(userQuota))
if ( userQuota.exhausted_requests >= userQuota.access_limit ) {
/** User has reached the quota limit. Deny the request. set with 401 or 403 status code */
} else {
/** User can access the API. call next() */
}
}
Of course, the snippet is is incomplete. It just gives you the idea about how to go about writing that middleware.
Here is how you can use the middleware for your APIs:
/** If requests to routes are under the quota */
app.get("/api/quota-routes", requireAuth, checkQuota, /** Mount the actual middleware here */)
/** If requests to routes are unlimited, just remove the checkQuota middleware */
app.get("/api/unlimited-routes", requireAuth, /** Mount the actual middleware here */)
Upvotes: 2