Adding A Rate Limiter Using Axum Governor
Adding A Rate Limiter Using Axum Governor
Adding A Rate Limiter Using Axum Governor

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:

  • an interested user loves your application, browses it too much till it crashes (pretty rare),
  • sometime sets up a bot to scrape your app's content (not really harmless),
  • an overzealous search engine bot in an attempt at spidering your app sends a huge amount of requests,
  • et cetera.

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:

  • Governor crate: the most infamous but complex
  • Tower-governor: a crate that integrates Governor into Tower
  • Axum-governor: an Axum-ready crate built on Governor.
  • tokio-rate-limit: a lock-free rate limiter crate.

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.