Any application that exposes its working to the general public is always at risk of being abused in one way or another. It's not just hacking into it. Sometimes, a malicious user or users will attempt to overwhelm it by flooding it with a deluge of requests. Also known as Denial of Service (DoS) Attack (or if they use multiple IP addresses to accomplish it becomes DDoS - Distributed Denial of Service Attack). In other instances, an infected computer harbors a malware that launches a deluge of requests to your application. But that is just the malicious part. Sometimes it is pretty harmless:
This is where the concept of Rate Limiting comes in. With rate limiting you can place a cap to the number of requests that a particular visitor to your app can send within a certain timespan (usually seconds for APIs or minutes). With that in place if a user or bot exceeds that cap within that timeframe you can deny access to them/it to prevent overburdening your app's resources. You can also stop overzealous search engine bots, scrapers or malicious users attempting to shut down your app.
There are several really good Rust crates that can help you integrate rate limiting capabilities to your app:
We will be using Axum-governor for this. It is relatively simple to integrate and can give us a single-shot global rate limiter for an entire app. NOTE: this is good enough for rather simple Rust applications. For robust Rust applications a global rate limiter is an anti-pattern; the best is to apply a rate limiter on a per-route, per-user basis which is rather complex. We will use the global limiter in this short tutorial.
First, set up the Axum-governor in your application using the following command:
cargo add axum-governor
Let's use a simple single-file, self-contained app as as example of setting up your rate-limiter:
//main.rs
use std::net::SocketAddr;
use std::env;
use axum::{
routing::{get, post},
Router,
middleware::from_fn_with_state,
};
//Rate limiter
use axum_governor::{GovernorConfigBuilder, GovernorLayer, Quota, nz, extractor::PeerIp};
#[tokio::main]
async fn main() {
//... Code redacted for brevity's sake
//Start rate limiter configuration
let ratelimiter = GovernorConfigBuilder::default()
.with_extractor(PeerIp::default())
.expect_connect_info()
/* .quota_default(Quota::requests_per_second(nz!(50u32))) = 50 requests per second */
.quota_default(Quota::requests_per_minute(nz!(50u32)))// = 50 requests per minute
.finish()
.unwrap();
// Get appname from environment variable or use default
let appname = std::env::var("APPNAME")
.unwrap_or_else(|_| {
tracing::warn!("APPNAME not found in environment, using default");
"My Awesome App".to_string()
});
// Create application state
let app_state = AppState::new(appname);
// Get server URL from environment variable or use default
let server_url = std::env::var("SERVERURL")
.unwrap_or_else(|_| {
tracing::warn!("SERVERURL not found in environment, using default");
"0.0.0.0:3000".to_string()
});
// Build the application and set our rate limiter
let app = Router::new()
.route("/", get(homepage))
.route("/some-other-route", get(some_other_controller))
.layer(GovernorLayer::new(ratelimiter))//Set rate limiter
.with_state(app_state);
// Start the server
let listener = match tokio::net::TcpListener::bind(&server_url).await {
Ok(listener) => listener,
Err(e) => {
tracing::error!("Failed to bind to {}: {}", server_url, e);
std::process::exit(1);
}
};
//tracing::info!("Server listening on http://{}", server_url);
println!("Server listening on http://{}", server_url);
if let Err(e) = axum::serve(listener, app.into_make_service_with_connect_info::())
.with_graceful_shutdown(shutdown_signal())
.await {
tracing::error!("Server error: {}", e);
std::process::exit(1);
}
}
Note the 'ratelimiter' variable. That's where we configure the global Axum-governor rate limiter options with the 'Quota:: ...' bit allowing us to set the number of maximum requests either per minute or second. That's all that is needed. You can build your application then run sample tests by hitting the '/' with numerous requests. If it shuts down after the maximum requests within the timespan you have specified then your rate limiter is working right.
For an in-depth understanding of rate limiters and their complex concepts (and if you really want to build your own rate limiter) you can read this Medium artilce: Building a Rate Limiter in Rust
WORDCOUNT: 701 words.