Ratelimitly is a distributed admission-control service. Before beginning work, an application can ask whether the work may consume one or more rate-limited resources. The same decision can also require recently measured service latencies to remain below specified thresholds.
The ratelimitly crate is the official Rust client. It exposes two independent
operations:
An application may use either operation without the other. A typical service requests admission, performs the work only after a grant, and optionally reports the measured latency afterward. A dedicated observer may only report latencies, while another application may only request resources.
flowchart LR
App["Application"] --> Request["Resource request<br/>consumptions + optional guards"]
Request --> Decision{"Ratelimitly decision"}
Decision -->|Granted| Work["Resources consumed<br/>perform work"]
Decision -->|Rejected| Stop["Nothing consumed<br/>do not perform work"]
Observer["Same or another application"] --> Report["Optional latency report"]
Report --> Trackers["Latency trackers"]
Trackers -. "read by guards" .-> Decision
A non-empty resource request has three application-level outcomes:
Failure is neither grant nor rejection. A failure can occur after Ratelimitly has received the request, so it does not prove that no resource was consumed. The application must explicitly choose its own fail-open, fail-closed, or fallback behavior.
The examples assume that the API key is stored in the
RATELIMITLY_API_KEY environment variable and that Tokio runs the async
application:
use ratelimitly::{ApiKey, Client};
let api_key: ApiKey = std::env::var("RATELIMITLY_API_KEY")?.parse()?;
let client = Client::builder(api_key).build().await?;
Never put an API key in source code, examples, command-line arguments, or logs.
In English: “Get me one token for checkout, whose limit is 100 tokens per
second.”
use std::time::Duration;
use ratelimitly::{Decision, Resource};
let checkout = Resource::new(
"checkout", // Stable application-defined resource name.
Duration::from_secs(1), // Rate-counter window.
100, // Tokens available in that window.
)?;
let response = client
.request()
.consume(&checkout, 1)? // Request one token.
.send()
.await?; // Failure is returned as Err.
match response.decision() {
Decision::Granted => perform_checkout().await?,
Decision::Rejected => return_service_busy(),
}
A grant consumes the token and authorizes perform_checkout; a rejection
consumes nothing. Result::Err represents failure to obtain a decision.
In English: “Record that one call to inventory took 18 ms.”
use std::time::Duration;
use ratelimitly::LatencyTracker;
let inventory = LatencyTracker::builder("inventory")
.sample_ttl(Duration::from_secs(10)) // Maximum sample lifetime.
.max_samples(100) // Samples considered by the tracker.
.buffer_size(32) // Requested tracker storage.
.min_samples(5) // Warm-up before guards take effect.
.build()?;
client
.report_latency(
&inventory, // Tracker receiving the sample.
Duration::from_millis(18), // Measured service duration.
)
.await?;
Measure the service operation with a monotonic clock—not the Ratelimitly request. The report does not request or consume a token.
In English: “Get me one token for checkout, but only if the tracked
inventory latency is below 100 ms.”
let response = client
.request()
.consume(&checkout, 1)?
.guard(
&inventory, // Same tracker definition as reports.
Duration::from_millis(100), // Admission requires latency < 100 ms.
)?
.send()
.await?;
match response.decision() {
Decision::Granted => perform_checkout().await?,
Decision::Rejected => return_service_busy(),
}
The resource consumption and guard form one atomic decision. If either fails, the complete request is rejected and no token is consumed.
Resource and guard counts are independent:
| Resources | Guards | Behavior |
|---|---|---|
| zero | zero | Local grant; no Ratelimitly request is made. |
| one or more | zero | Resource-consumption request. |
| zero | one or more | Guard-only request. |
| one or more | one or more | Atomic combined decision. |
See Concepts for resource identities, latency trackers, and API-key limits.
Add the crate with Cargo:
cargo add ratelimitly
The client is asynchronous and uses Tokio. It requires Rust 1.88 or newer and uses the Rust 2024 edition. Raising the minimum supported Rust version is a breaking change and will be reflected in the crate’s semantic versioning.
The supported native targets are:
x86_64-unknown-linux-gnu and aarch64-unknown-linux-gnu;x86_64-apple-darwin and aarch64-apple-darwin; andx86_64-pc-windows-msvc.Other targets are best effort. The first public release is blocked on CI for the supported platform matrix and Rust 1.88 as well as current stable Rust.
Licensed under the MIT License.