13 unstable releases (6 breaking)

0.7.0 Sep 17, 2024
0.6.0 May 3, 2024
0.6.0-rc.1 Apr 22, 2024
0.5.0 Sep 13, 2023
0.1.1 Jul 29, 2020

#424 in Web programming

Download history 37/week @ 2024-09-09 161/week @ 2024-09-16 56/week @ 2024-09-23 9/week @ 2024-09-30 1/week @ 2024-10-07 5/week @ 2024-10-14

596 downloads per month

Apache-2.0

245KB
6K SLoC

Limitador (library)

Crates.io docs.rs

An embeddable rate-limiter library supporting in-memory, Redis and disk data stores.

For the complete documentation of the crate's API, please refer to docs.rs

Features

  • redis_storage: support for using Redis as the data storage backend.
  • disk_storage: support for using RocksDB as a local disk storage backend.
  • lenient_conditions: support for the deprecated syntax of Conditions
  • default: redis_storage.

lib.rs:

Limitador is a generic rate-limiter.

Basic operation

Limitador can store the counters in memory or in Redis. Storing them in memory is faster, but the counters cannot be shared between several instances of Limitador. Storing the limits in Redis is slower, but they can be shared between instances.

By default, the rate limiter is configured to store the counters in memory. It'll store only a limited amount of "qualified counters", specified as a u64 value in the constructor.

use limitador::RateLimiter;
let rate_limiter = RateLimiter::new(1000);

To use Redis:

#[cfg(feature = "redis_storage")]
use limitador::RateLimiter;
use limitador::storage::redis::RedisStorage;

// Default redis URL (redis://localhost:6379).
let rate_limiter = RateLimiter::new_with_storage(Box::new(RedisStorage::default()));

// Custom redis URL
let rate_limiter = RateLimiter::new_with_storage(
    Box::new(RedisStorage::new("redis://127.0.0.1:7777").unwrap())
);

Limits

The definition of a limit includes:

  • A namespace that identifies the resource to limit. It could be an API, a Kubernetes service, a proxy ID, etc.
  • A value.
  • The length of the period in seconds.
  • Conditions that define when to apply the limit.
  • A set of variables. For example, if we need to define the same limit for each "user_id", instead of creating a limit for each hardcoded ID, we just need to define "user_id" as a variable.

If we used Limitador in a context where it receives an HTTP request we could define a limit like this to allow 10 requests per minute and per user_id when the HTTP method is "GET".

use limitador::limit::Limit;
let limit = Limit::new(
    "my_namespace",
     10,
     60,
     vec!["req.method == 'GET'"],
     vec!["user_id"],
);

Notice that the keys and variables are generic, so they do not necessarily have to refer to an HTTP request.

Manage limits

use limitador::RateLimiter;
use limitador::limit::Limit;
let limit = Limit::new(
    "my_namespace",
     10,
     60,
     vec!["req.method == 'GET'"],
     vec!["user_id"],
);
let mut rate_limiter = RateLimiter::new(1000);

// Add a limit
rate_limiter.add_limit(limit.clone());

// Delete the limit
rate_limiter.delete_limit(&limit);

// Get all the limits in a namespace
let namespace = "my_namespace".into();
rate_limiter.get_limits(&namespace);

// Delete all the limits in a namespace
rate_limiter.delete_limits(&namespace);

Apply limits

use limitador::RateLimiter;
use limitador::limit::Limit;
use std::collections::HashMap;

let mut rate_limiter = RateLimiter::new(1000);

let limit = Limit::new(
    "my_namespace",
     2,
     60,
     vec!["req.method == 'GET'"],
     vec!["user_id"],
);
rate_limiter.add_limit(limit);

// We've defined a limit of 2. So we can report 2 times before being
// rate-limited
let mut values_to_report: HashMap<String, String> = HashMap::new();
values_to_report.insert("req.method".to_string(), "GET".to_string());
values_to_report.insert("user_id".to_string(), "1".to_string());

// Check if we can report
let namespace = "my_namespace".into();
assert!(!rate_limiter.is_rate_limited(&namespace, &values_to_report, 1).unwrap());

// Report
rate_limiter.update_counters(&namespace, &values_to_report, 1).unwrap();

// Check and report again
assert!(!rate_limiter.is_rate_limited(&namespace, &values_to_report, 1).unwrap());
rate_limiter.update_counters(&namespace, &values_to_report, 1).unwrap();

// We've already reported 2, so reporting another one should not be allowed
assert!(rate_limiter.is_rate_limited(&namespace, &values_to_report, 1).unwrap());

// You can also check and report if not limited in a single call. It's useful
// for example, when calling Limitador from a proxy. Instead of doing 2
// separate calls, we can issue just one:
rate_limiter.check_rate_limited_and_update(&namespace, &values_to_report, 1, false).unwrap();

Async

There are two Redis drivers, a blocking one and an async one. To use the async one, we need to instantiate an "AsyncRateLimiter" with an "AsyncRedisStorage":

#[cfg(feature = "redis_storage")]
use limitador::AsyncRateLimiter;
use limitador::storage::redis::AsyncRedisStorage;

async {
    let rate_limiter = AsyncRateLimiter::new_with_storage(
        Box::new(AsyncRedisStorage::new("redis://127.0.0.1:7777").await.unwrap())
    );
};

Both the blocking and the async limiters expose the same functions, so we can use the async limiter as explained above. For example:

#[cfg(feature = "redis_storage")]
use limitador::AsyncRateLimiter;
use limitador::limit::Limit;
use limitador::storage::redis::AsyncRedisStorage;
let limit = Limit::new(
     "my_namespace",
     10,
     60,
     vec!["req.method == 'GET'"],
     vec!["user_id"],
);

async {
    let rate_limiter = AsyncRateLimiter::new_with_storage(
        Box::new(AsyncRedisStorage::new("redis://127.0.0.1:7777").await.unwrap())
    );
    rate_limiter.add_limit(limit);
};

Limits accuracy

When storing the counters in memory, Limitador guarantees that we'll never go over the limits defined. However, when using Redis that's not the case. The Redis driver sacrifices a bit of accuracy when applying the limits to be more performant.

Dependencies

~8–26MB
~404K SLoC