express-throttle

2.0.0 • Public • Published

express-throttle

Request throttling middleware for Express framework

npm version Build Status

Installation

$ npm install express-throttle

Implementation

The throttling is done using the canonical token bucket algorithm, where tokens are "refilled" in a sliding window manner by default (can be configured to fixed time windows). This means that if we a set maximum rate of 10 requests / minute, a client will not be able to send 10 requests 0:59, and 10 more 1:01. However, if the client sends 10 requests at 0:30, he will be able to send a new request at 0:36 (since tokens are refilled continuously 1 every 6 seconds).

Limitations

By default, throttling data is stored in memory and is thus not shared between multiple processes. If your application is behind a load balancer which distributes traffic among several node processes, then throttling will be applied per process, which is generally not what you want (unless you can ensure that a client always hits the same process). It is possible to customize the storage so that the throttling data gets saved to a shared backend (e.g Redis). However, the current implementation contains a race-condition and will likely fail (erroneously allow/block certain requests) under high load. My plan is to address this shortcoming in future versions.

TL;DR - Use this package in production at your own risk, beware of the limitations.

  • If you are running node as single process = you should be fine
  • If you are running node as multiple processes = be aware that throttling is done per process
  • If you have configured to use an external backend = some requests may erroneously be throttled (or passed through) under high load conditions

Examples

var express = require("express");
var throttle = require("express-throttle");
  
var app = express();
  
// Allow 5 requests at any rate with an average rate of 5/s
app.post("/search", throttle({ "rate": "5/s" }), function(req, res, next) {
  // ...
});
 
// Allow 5 requests at any rate during a fixed time window of 1 sec 
app.post("/search", throttle({ "burst": 5, "period": "1s" }), function(req, res, next) {
  // ...
});

Combine it with a burst capacity of 10, meaning that the client can make 10 requests at any rate. The burst capacity is "refilled" with the specified rate (in this case 5/s).

app.post("/search", throttle({ "burst": 10, "rate": "5/s" }), function(req, res, next) {
  // ...
});
  
// "Half" requests are supported as well (1 request every other second)
app.post("/search", throttle({ "burst": 5, "rate": "1/2s" }), function(req, res, next) {
  // ...
});

By default, throttling is done on a per ip-address basis (see this link about how the ip address is extracted from the request). This can be configured by providing a custom key-function:

var options = {
  "burst": 10,
  "rate": "5/s",
  "key": function(req) {
    return req.session.username;
  }
};
 
app.post("/search", throttle(options), function(req, res, next) {
  // ...
});

The "cost" per request can also be customized, making it possible to, for example whitelist certain requests:

var whitelist = ["ip-1", "ip-2", ...];
  
var options = {
  "burst": 10,
  "rate": "5/s",
  "cost": function(req) {
    var ip_address = req.connection.remoteAddress;
      
    if (whitelist.indexOf(ip_address) >= 0) {
      return 0;
    } else if (req.session.is_privileged_user) {
      return 0.5;
    } else {
      return 1;
    }
  }
};
  
app.post("/search", throttle(options), function(req, res, next) {
  // ...
});
 
var options = {
  "rate": "1/s",
  "burst": 10,
  "cost": 2.5 // fixed costs are also supported
};
 
app.post("/expensive", throttle(options), function(req, res, next) {
  // ...
});

Throttled requests will simply be responded with an empty 429 response. This can be overridden:

var options = {
  "burst": 5
  "period": "1min",
  "on_throttled": function(req, res, next, bucket) {
    // Possible course of actions:
    // 1) Log request
    // 2) Add client ip address to a ban list
    // 3) Send back more information
    res.set("X-Rate-Limit-Limit", 5);
    res.set("X-Rate-Limit-Remaining", 0);
    // bucket.etime = expiration time in Unix epoch ms, only available
    // for fixed time windows
    res.set("X-Rate-Limit-Reset", bucket.etime);
    res.status(503).send("System overloaded, try again at a later time.");
  }
};

You may also customize the response on requests that are passed through:

var options = {
  "rate": "5/s",
  "on_allowed": function(req, res, next, bucket) {
    res.set("X-Rate-Limit-Limit", 5);
    res.set("X-Rate-Limit-Remaining", bucket.tokens);
  }
}

Throttling can be applied across multiple processes. This requires an external storage mechanism which can be configured as follows:

function ExternalStorage(connection_settings) {
  // ...
}
 
// These methods must be implemented
ExternalStorage.prototype.get = function(key, callback) {
  fetch(key, function(bucket) {
    // First argument should be null if no errors occurred
    callback(null, bucket);
  });
}
  
ExternalStorage.prototype.set = function(key, bucket, lifetime, callback) {
  save(key, bucket, function(err) {
    // err should be null if no errors occurred
    callback(err); 
  });
}
  
var options = {
  "rate": "5/s",
  "store": new ExternalStorage()
}
  
app.post("/search", throttle(options), function(req, res, next) {
  // ...
});

Options

burst: The number of requests that can be made at any rate. Defaults to X as defined below for rolling windows.

rate: Determines the rate at which the burst quota is "refilled". This will control the average number of requests per time unit. Must be specified according to the following format: X/Yt

where X and Y are integers and t is the time unit which can be any of the following: ms, s, sec, m, min, h, hour, d, day

E.g 5/s, 180/15min, 1000/d

period: The duration of the time window after which the entire burst quota is refilled. Must be specified according to the following format: Y/t, where Y and t are defined as above.

E.g 5s, 15min, 1000d

store: Custom storage class. Must implement a get and set method with the following signatures:

// callback in both methods must be called with an error (if any) as first argument
 
function get(key, callback) {
  fetch(key, function(err, bucket) {
    if (err) callback(err);
    else callback(null, bucket);
  });
}
function set(key, bucket, callback) {
  // 'bucket' will be an object with the following structure:
  /*
    {
      "tokens": Number, (current number of tokens)
      "mtime": Number, (last modification time)
      "etime": Number (expiration time, only available for fixed time windows)
    }
  */
  save(key, bucket, function(err) {
    callback(err);
  }
}

Defaults to an LRU cache with a maximum of 10000 entries.

store_size: Determines the maximum number of entries for the default in-memory LRU cache. 0 indicates no limit, not recommended, since entries in the cache are not expired / cleaned up / garbage collected.

key: Function used to identify clients. It will be called with an express request object. Defaults to:

function(req) {
    return req.ip; // http://expressjs.com/en/api.html#req.ip
}

cost: Number or function used to calculate the cost for a request with an express request object. Defaults to 1.

on_allowed: A function called when the request is passed through with an express request object, express response object, next function and a bucket object. Defaults to:

function(req, res, next, bucket) {
    next();
}

on_throttled: A function called when the request is throttled with an express request object, express response object, next function and a bucket object. Defaults to:

function(req, res, next, bucket) {
    res.status(429).end();
};

Package Sidebar

Install

npm i express-throttle

Weekly Downloads

3,084

Version

2.0.0

License

MIT

Last publish

Collaborators

  • glurg