| Crates.io | tower-shot |
| lib.rs | tower-shot |
| version | 0.1.1 |
| created_at | 2026-01-22 09:08:58.677587+00 |
| updated_at | 2026-01-22 16:46:18.086965+00 |
| description | A high-performance, atomic-backed rate limiting middleware for `tower` |
| homepage | https://github.com/garypen/rate-limiting |
| repository | https://github.com/garypen/rate-limiting |
| max_upload_size | |
| id | 2061156 |
| size | 139,275 |
A high-performance, atomic-backed rate limiting middleware for tower and axum that provides choice between throughput or latency.
There's a gap in the rate limiting marketplace for a rate limiter which optimises throughput within specified timeout constraints.
You can't do this with standard native components, because the tower rate limiter is not Clone. This requires the use of tower::buffer::Buffer to make a service stack shareable. This introduces significant challenges:
Buffer before or after other layers (like Timeout) changes the failure semantics (e.g., does the timeout include the queue time?).Buffer becomes a bottleneck.If you decide to use tower-governor, this provides an integrated solution, but it's latency focussed and there doesn't appear to be a way to tweak the rate limiting configuration to try and maximise throughput.
tower-shot solves these issues by providing atomic, lock-free strategies and a choice of implementations that eliminate the need for buffers entirely.
tower-shot offers three distinct ways to handle rate limiting, depending on your application's priorities:
| Mode | Component | Best For... | Behavior |
|---|---|---|---|
| Raw | RateLimitLayer |
Standard Compliance | A drop-in replacement for tower::limit::RateLimit. Returns Poll::Pending when full. Requires you to handle backpressure (usually via LoadShed). |
| Latency | make_latency_svc |
Protecting P99 / SLA | Fail Fast. If the limit is reached, it rejects the request immediately (in nanoseconds). No queuing. Keeps accepted requests fast. |
| Throughput | make_timeout_svc |
Max Throughput | Wait & Retry. If the limit is reached, it retries until a permit is available or the timeout is reached. Bounds the wait time. |
We subjected the system to a massive burst of 50,000 concurrent requests against a 10,000 req/s limit.
Using RateLimitLayer directly (no Buffer).
Unlike native Tower, tower-shot services are cloneable and share state without a Buffer.
Poll::Pending. The runtime handles the "queue" of waiting tasks.Using make_latency_svc.
Using make_timeout_svc.
Using tower::limit::RateLimit wrapped in tower::buffer::Buffer.
| Metric | Raw (Direct) | Managed Throughput | Managed Latency |
|---|---|---|---|
| Success Rate | ~6,260 req/s | ~6,035 req/s | ~2,500 req/s |
| P50 Ready Time | ~2.0 s | ~1.0 s | 125 ns |
| P99 Ready Time | ~4.0 s | ~3.0 s | 541 ns |
| Failure Mode | Unbounded Waiting | Timeout | Immediate Rejection |
tower-shot is designed to be the fastest rate limiter in the ecosystem.
When 1,000 concurrent tasks compete for a permit, tower-shot's atomic design shines.
tower-shot (Managed): ~172 µsgovernor (Managed): ~317 µstower (Native Managed): ~13,500 µs (13.5 ms)Result:
tower-shotis ~80x faster than native Tower and ~1.8x faster than Governor under high contention.
When the system is overloaded, how fast can we say "No"?
tower-shot: ~125 nsgovernor: ~265 ns[dependencies]
tower-shot = "0.1.0"
shot-limit = "0.1.0"
tower = "0.5"
axum = "0.8"
tokio = { version = "1", features = ["full"] }
Use this when you want to handle as many requests as possible, even if they have to wait a bit.
use std::time::Duration;
use std::sync::Arc;
use shot_limit::TokenBucket;
use tower_shot::make_timeout_svc;
// 1. Define Strategy: 100 requests per second
let strategy = Arc::new(TokenBucket::new(100, 10, Duration::from_secs(1)));
// 2. Wrap your service
// Requests will wait up to 500ms for a permit before timing out.
let service = make_timeout_svc(
strategy,
Duration::from_millis(500),
my_service
);
Use this for critical APIs where a slow response is worse than an error.
use shot_limit::FixedWindow;
use tower_shot::make_latency_svc;
// 1. Define Strategy
let strategy = Arc::new(FixedWindow::new(100, Duration::from_secs(1)));
// 2. Wrap your service
// Requests are rejected IMMEDIATELY if the limit is exceeded.
let service = make_latency_svc(strategy, my_service);
Use this with axum or ServiceBuilder if you want to manage the stack manually.
use tower::ServiceBuilder;
use tower_shot::RateLimitLayer;
let app = Router::new()
.route("/", get(|| async { "Hello!" }))
.layer(
ServiceBuilder::new()
.timeout(Duration::from_secs(2)) // Handle the wait
.layer(RateLimitLayer::new(strategy)) // Returns Pending when full
);
tower-shot provides a unified ShotError that integrates with axum.
| Error | HTTP Status | Meaning |
|---|---|---|
ShotError::Overloaded |
503 Service Unavailable |
Limit reached (Latency mode). |
ShotError::Timeout |
408 Request Timeout |
Wait time exceeded (Throughput mode). |
ShotError::Inner(e) |
500 Internal Server Error |
Application error. |
Licensed under either of Apache License, Version 2.0 or MIT license at your option.