rust
61 lines · 9 steps
Rate-limiting an expensive Axum route with tower
An Axum PDF-rendering endpoint bounds concurrency and enforces timeouts by layering tower middleware onto a sub-router.
Explained by
highlit
1use std::time::Duration;
2
3use axum::{
4 extract::State,
5 http::StatusCode,
6 response::{IntoResponse, Json},
7 routing::post,
8 Router,
9};
10use serde::{Deserialize, Serialize};
11use tower::limit::ConcurrencyLimitLayer;
12
13#[derive(Clone)]
14struct AppState {
15 renderer: RenderClient,
16}
17
18#[derive(Deserialize)]
19struct RenderRequest {
20 template: String,
21 #[serde(default)]
22 payload: serde_json::Value,
23}
24
25#[derive(Serialize)]
26struct RenderResponse {
27 document_id: String,
28 bytes: usize,
29}
30
31async fn render_pdf(
32 State(state): State<AppState>,
33 Json(req): Json<RenderRequest>,
34) -> Result<Json<RenderResponse>, StatusCode> {
35 let rendered = state
36 .renderer
37 .render(&req.template, req.payload)
38 .await
39 .map_err(|_| StatusCode::BAD_GATEWAY)?;
40
41 Ok(Json(RenderResponse {
42 document_id: rendered.id,
43 bytes: rendered.body.len(),
44 }))
45}
46
47async fn health() -> impl IntoResponse {
48 (StatusCode::OK, "ok")
49}
50
51pub fn build_router(state: AppState) -> Router {
52 let expensive = Router::new()
53 .route("/render/pdf", post(render_pdf))
54 .layer(ConcurrencyLimitLayer::new(8))
55 .layer(tower_http::timeout::TimeoutLayer::new(Duration::from_secs(30)));
56
57 Router::new()
58 .route("/health", axum::routing::get(health))
59 .merge(expensive)
60 .with_state(state)
61}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Layers applied to a sub-router protect only its routes, so cheap endpoints like health checks stay unthrottled.
- 2tower middleware such as concurrency limits and timeouts composes onto Axum routers without touching handler logic.
- 3Axum extractors turn shared state and request bodies into typed handler arguments while the return type maps errors to HTTP responses.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
rust
use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Lines, Write};
K-way merge of sorted logs in Rust
binary-heap
k-way-merge
streaming-io
Intermediate
8 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
rust
use std::f64::consts::PI; #[derive(Debug, Clone, Copy)] pub struct LatLng {
Building geographic bounding boxes in Rust
geospatial
value-types
option
Intermediate
7 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/rate-limiting-an-expensive-axum-route-with-tower-explained-rust-fecb/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.