A Simple Rate Limiter for CloudFlare Workers (Serverless API) based on KV Stores


CloudFlare Workers is a Serverless Technology. We can use it to host the Serverless APIs. However, no matter what the status code or content is returned from the cloudflare worker, it will be counted as one request because the worker function is hit. Therefore, it is necessary to rate limit the CloudFlare Worker APIs in order to avoid surprising billing.

Please note that, the Free Tier of CloudFlare worker has a daily 100K requests cap. Requests exceeding that threshold will fail (either soft or hard returning 1027 status code). For Paid plan, there is no Usage Cap, but we can always set a Usage-based Notification once the number of requests exceeds a threshold.

Using CloudFlare Rate Limiter Product to Rate Limiting the CLoudFlare Workers

The CloudFlare provides a inhouse Rate Limiter. However, in order to use the CloudFlare Rate Limiter on Worker, we have to bind the worker functions to a domain (and add a route) first in order to configure and enable the rate limiting.

Currently, CloudFlare Rate Limiter charges like 2 cent per 10K requests, so maybe it is better to utilize this feature without managing the rate limiting ourselves.

A Simple Rate Limiter for CloudFlare (Serverless API) based on KV Stores

CloudFlare provides a KV Store (Key Value) which is a eventually consistent data storage. In order to use this, we have to first create a namespace in Workers/KV and then bind it to the worker function (under Workers/Settings/Variables). Assume we have binded the NAMESPACE to KV, then we can add the following Rate Limiter logics at the begining of handling the requests.

The following Javascript code checks the number of the requests per IP address for any 60 second window, and returns 429 Too Many Requests if the number exceeds the threshold MAX_REQUESTS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async function handleRequest(request) {
  let res;
  const MAX_REQUESTS = 60;
  const ip = request.headers.get("CF-Connecting-IP");
 
  let value = await KV.get(ip)
  if (value === null) {
    value = 1;
  }
  if (value >= MAX_REQUESTS) {
      res = new Response(null, {
        status: 429,
        statusText: 'Too Many Requests',
      });
      res.headers.set('Access-Control-Allow-Origin', '*');
      res.headers.set('Cache-Control', 'max-age=3');    
      return res;
  }
 
  try {    
    await KV.put(ip, parseInt(value) + 1, { expirationTtl: 61 });
  } catch (ex) {
    // ignore - as the KV threshold may exceed
    console.log(ex);
  }  
  // the main Worker API logics 
  // ...
}
async function handleRequest(request) {
  let res;
  const MAX_REQUESTS = 60;
  const ip = request.headers.get("CF-Connecting-IP");

  let value = await KV.get(ip)
  if (value === null) {
    value = 1;
  }
  if (value >= MAX_REQUESTS) {
      res = new Response(null, {
        status: 429,
        statusText: 'Too Many Requests',
      });
      res.headers.set('Access-Control-Allow-Origin', '*');
      res.headers.set('Cache-Control', 'max-age=3');    
      return res;
  }

  try {    
    await KV.put(ip, parseInt(value) + 1, { expirationTtl: 61 });
  } catch (ex) {
    // ignore - as the KV threshold may exceed
    console.log(ex);
  }  
  // the main Worker API logics 
  // ...
}

However, there are limitations:

  • The expirationTtl parameter setting the key-value expires in seconds from now – should be set to at least 60 or more. This is due to that the CloudFlare KV will be propagated to edge servers no more than 60 seconds.
  • The KV supports unlimited reads and writes (different keys). For same keys, max 1 write per second. For Free Tier, there is a daily cap of 100K reads and 1000 writes. Therefore, we have to put the KV.put in a try-catch to ignore the failure especially if it is under Free Tier.
  • The KV is an event consistent storage, so the read may reflect to an old state. So it might not work precisely.
  • Since there is only 1 write per second, thus any MAX REQUESTS larger than 60 may not be actually hit.
  • We use the IP address as the keys (the buckets), so therefore might not work the best especially if an IP address is shared among many.

But, still, better than nothing. We can still apply the above simple rate limiter logics to avoid flood spam the CloudFlare Worker!

Enhanced Rate Limiter (Sliding Window) using Cloudflare Worker

The above rate limiter is simple – how may not work as expected. Consider if a request is sent every 59 seconds, and after 60 * 59 seconds, the 429 Too Many Requests Error may still be returned unexpectedly. The KV expires in 61 seconds, and thus, a request before expiry will renew the expiry for another 61 seconds and increment the counter.

To resolve this, we store the requests timestamps (as an array or FIFO queue) in the KV store, and then, each request, we pop out the expired timestamps/requests, and check the size of the array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
async function handleRequest(request) {
  let res;
  const MAX_REQUESTS = 60;
  const WINDOW_SIZE = 60;
  const country = request.headers.get('cf-ipcountry');
  const ip = request.headers.get("CF-Connecting-IP");
 
  let value = null;
  try {
    value = JSON.parse(await KV.get(ip));
  } catch (e) {
    console.log(e);
  }
  if ((value === null) || (!Array.isArray(value))) {
    value = [];
  }
  // deque the expired requests
  while ((value.length > 0) && (Date.now() - value[0] > WINDOW_SIZE * 1000)) {
    value.shift();
  }
  if (value.length >= MAX_REQUESTS) {
    res = new Response(JSON.stringify( { error: "Too Many Requests", "errorCode": 429 }), {
      status: 429,
      statusText: 'Too Many Requests',
    });
    res.headers.set('Access-Control-Allow-Origin', '*');
    res.headers.set('Cache-Control', 'max-age=3');    
    res.headers.set("Country", country);
    res.headers.set("Count", value.length);
    res.headers.set("IP", ip);
    return res;
  }
 
  value.push(Date.now());
  try {        
    await KV.put(ip, JSON.stringify(value), {expirationTtl: 61});
  } catch (ex) {
    // ignore - as the KV threshold may exceed
    console.log(ex);
  }
  // the main Worker API logics 
  // ...
}
async function handleRequest(request) {
  let res;
  const MAX_REQUESTS = 60;
  const WINDOW_SIZE = 60;
  const country = request.headers.get('cf-ipcountry');
  const ip = request.headers.get("CF-Connecting-IP");

  let value = null;
  try {
    value = JSON.parse(await KV.get(ip));
  } catch (e) {
    console.log(e);
  }
  if ((value === null) || (!Array.isArray(value))) {
    value = [];
  }
  // deque the expired requests
  while ((value.length > 0) && (Date.now() - value[0] > WINDOW_SIZE * 1000)) {
    value.shift();
  }
  if (value.length >= MAX_REQUESTS) {
    res = new Response(JSON.stringify( { error: "Too Many Requests", "errorCode": 429 }), {
      status: 429,
      statusText: 'Too Many Requests',
    });
    res.headers.set('Access-Control-Allow-Origin', '*');
    res.headers.set('Cache-Control', 'max-age=3');    
    res.headers.set("Country", country);
    res.headers.set("Count", value.length);
    res.headers.set("IP", ip);
    return res;
  }

  value.push(Date.now());
  try {        
    await KV.put(ip, JSON.stringify(value), {expirationTtl: 61});
  } catch (ex) {
    // ignore - as the KV threshold may exceed
    console.log(ex);
  }
  // the main Worker API logics 
  // ...
}

The above Rate Limiter is based on the Sliding Window Algorithm – where we keep a fixed size of window e.g. 60 seconds.

CloudFlare Technology

–EOF (The Ultimate Computing & Technology Blog) —

GD Star Rating
loading...
1051 words
Last Post: Teaching Kids Programming - Number of Zero-Filled Subarrays (GroupBy Algorithm + Math Counting)
Next Post: Teaching Kids Programming - Reordered Power of Two (Rearranging the Digits, Permutation + Counting)

The Permanent URL is: A Simple Rate Limiter for CloudFlare Workers (Serverless API) based on KV Stores

Leave a Reply