Skip to content

Instantly share code, notes, and snippets.

@jeremybradbury
Last active February 19, 2021 02:42
Show Gist options
  • Save jeremybradbury/7a4d33f8d5da66848ab6c919b927dddf to your computer and use it in GitHub Desktop.
Save jeremybradbury/7a4d33f8d5da66848ab6c919b927dddf to your computer and use it in GitHub Desktop.
Simple async request rate limit middleware, for ExpressJS endpoints, using Redis with 1 second TTLs
////
// MIT License
// Copyright (c) 2021 Jeremy Bradbury
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
////
// requires redis-server running
const redis = require("redis"); // requires the redis client in your package.json
const { promisify } = require("util"); // all redis client callbacks conform to promisify's requirements
const client = redis.createClient();
const cGet = promisify(client.get).bind(client);
const cIncr = promisify(client.incr).bind(client);
const cExpire = promisify(client.expire).bind(client);
const defaultRate = 1; // 1 req per second, per ip
// modified from: https://redis.io/commands/incr#pattern-rate-limiter
const rateLimit = async (ip, next, reqPerScond = defaultRate) => {
const ts = Date.now().toString().slice(0, 10); // seconds beyond epoch, strip the last 3 (ms)
const key = `${ip}@${ts}`; // squish ip@timestamp together, doesn't matter how if it's always the same
const current = await cGet(key); // were there requests this second?
if (current && current > reqPerScond) {
// were there too many?
return false; // fail: send 429
}
await cIncr(key); // make a 1 or add to it
// we only need to know about requests in the current second
await cExpire(key, 1); // so keep only 1 key/value per public ip, for only 1 second
next(); // no need to await
return true; // success: next was called
};
// 1 req per second, per ip
const throttleByIp = async (req, res, next) => {
const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; // in nginx we trust, all others are suspect!
if (!(await rateLimit(ip, next, req.perSecond || defaultRate))) {
// next is passed to rate limit
return res.sendStatus(429); // 429 - Too Many Requests
} // next already called
}; // core middleware used by exported wrapper functions
// 10 reqs per second, per ip
const fastThrottle = async (req, res, next) => {
req.perSecond = 10;
return await throttleByIp(req, res, next); // return base middleware, rather than requiring both per endpoint
};
// 3 reqs per second, per ip
const normalThrottle = async (req, res, next) => {
req.perSecond = 3;
return await throttleByIp(req, res, next); // return base middleware, rather than requiring both per endpoint
};
// 1 req per second, per ip
const slowThrottle = throttleByIp; // defaultRate === 1, use base middleware
// add more, change the rates or strip down
module.exports = {
fastThrottle, // 10 requests / ip / sec
normalThrottle, // 3 requests / ip / sec
slowThrottle, // 1 request / ip / sec # equal to throttleByIp()
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment