Build or update the BlueBubbles external channel plugin for Moltbot (extension package, REST...
npx skills add salvo-rs/salvo-skills --skill "salvo-timeout"
Install specific skill from multi-skill repository
# Description
Configure request timeouts to prevent slow requests from blocking resources. Use for protecting APIs from long-running operations.
# SKILL.md
name: salvo-timeout
description: Configure request timeouts to prevent slow requests from blocking resources. Use for protecting APIs from long-running operations.
Salvo Request Timeout
This skill helps configure request timeouts in Salvo applications to prevent slow requests from consuming server resources indefinitely.
Why Use Timeouts?
- Prevent resource exhaustion: Long-running requests can consume connections and memory
- Improve reliability: Fail fast rather than hang indefinitely
- Better user experience: Users get quick feedback instead of waiting forever
- Protection against attacks: Slowloris and similar attacks are mitigated
Setup
Timeout is built into Salvo core:
[dependencies]
salvo = "1.88.1"
Basic Timeout
use std::time::Duration;
use salvo::prelude::*;
#[handler]
async fn fast_handler() -> &'static str {
"Hello, World!"
}
#[handler]
async fn slow_handler() -> &'static str {
// Simulates a slow operation
tokio::time::sleep(Duration::from_secs(10)).await;
"This takes a while..."
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().init();
let router = Router::new()
// Apply 5-second timeout to all routes
.hoop(Timeout::new(Duration::from_secs(5)))
.push(Router::with_path("fast").get(fast_handler))
.push(Router::with_path("slow").get(slow_handler));
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
When accessing /slow, the request will timeout after 5 seconds and return a 408 Request Timeout response.
Route-Specific Timeouts
Apply different timeouts to different routes:
use std::time::Duration;
use salvo::prelude::*;
#[tokio::main]
async fn main() {
let router = Router::new()
.push(
Router::with_path("quick")
.hoop(Timeout::new(Duration::from_secs(2)))
.get(quick_handler)
)
.push(
Router::with_path("standard")
.hoop(Timeout::new(Duration::from_secs(30)))
.get(standard_handler)
)
.push(
Router::with_path("long-running")
.hoop(Timeout::new(Duration::from_secs(300)))
.get(long_running_handler)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Timeout with API Routes
use std::time::Duration;
use salvo::prelude::*;
#[tokio::main]
async fn main() {
// Short timeout for API endpoints
let api_timeout = Timeout::new(Duration::from_secs(10));
// Longer timeout for file uploads
let upload_timeout = Timeout::new(Duration::from_secs(120));
let router = Router::new()
.push(
Router::with_path("api")
.hoop(api_timeout)
.push(Router::with_path("users").get(list_users))
.push(Router::with_path("products").get(list_products))
)
.push(
Router::with_path("upload")
.hoop(upload_timeout)
.post(handle_upload)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Global Timeout with Exceptions
use std::time::Duration;
use salvo::prelude::*;
#[tokio::main]
async fn main() {
// Default 30-second timeout
let default_timeout = Timeout::new(Duration::from_secs(30));
let router = Router::new()
.hoop(default_timeout) // Apply to all routes
.push(Router::with_path("health").get(health_check))
.push(Router::with_path("api/{**rest}").get(api_handler))
.push(
// Override with longer timeout for specific route
Router::with_path("reports/generate")
.hoop(Timeout::new(Duration::from_secs(300)))
.post(generate_report)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Timeout with Custom Error Response
Handle timeout errors with a custom catcher:
use std::time::Duration;
use salvo::prelude::*;
use salvo::catcher::Catcher;
#[handler]
async fn handle_timeout(res: &mut Response, ctrl: &mut FlowCtrl) {
if res.status_code() == Some(StatusCode::REQUEST_TIMEOUT) {
res.render(Json(serde_json::json!({
"error": "Request Timeout",
"message": "The request took too long to process",
"code": 408
})));
ctrl.skip_rest();
}
}
#[tokio::main]
async fn main() {
let router = Router::new()
.hoop(Timeout::new(Duration::from_secs(5)))
.get(slow_handler);
let service = Service::new(router).catcher(
Catcher::default().hoop(handle_timeout)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(service).await;
}
Combining Timeout with Other Middleware
use std::time::Duration;
use salvo::prelude::*;
use salvo::rate_limiter::{BasicQuota, FixedGuard, MokaStore, RateLimiter, RemoteIpIssuer};
#[tokio::main]
async fn main() {
// Rate limiter
let limiter = RateLimiter::new(
FixedGuard::new(),
MokaStore::new(),
RemoteIpIssuer,
BasicQuota::per_second(10),
);
// Timeout
let timeout = Timeout::new(Duration::from_secs(30));
let router = Router::new()
.hoop(limiter) // Rate limit first
.hoop(timeout) // Then apply timeout
.push(Router::with_path("api/{**rest}").get(api_handler));
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Practical Timeout Values
| Endpoint Type | Recommended Timeout |
|---|---|
| Health checks | 1-2 seconds |
| Simple API calls | 5-10 seconds |
| Database queries | 10-30 seconds |
| File uploads | 60-300 seconds |
| Report generation | 120-600 seconds |
| Real-time endpoints | Consider no timeout |
Example: Microservices Gateway
use std::time::Duration;
use salvo::prelude::*;
#[tokio::main]
async fn main() {
let router = Router::new()
// Auth service - should be fast
.push(
Router::with_path("auth/{**rest}")
.hoop(Timeout::new(Duration::from_secs(5)))
.goal(auth_proxy)
)
// User service - moderate
.push(
Router::with_path("users/{**rest}")
.hoop(Timeout::new(Duration::from_secs(15)))
.goal(users_proxy)
)
// Analytics service - can be slow
.push(
Router::with_path("analytics/{**rest}")
.hoop(Timeout::new(Duration::from_secs(60)))
.goal(analytics_proxy)
)
// Report service - long running
.push(
Router::with_path("reports/{**rest}")
.hoop(Timeout::new(Duration::from_secs(300)))
.goal(reports_proxy)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Best Practices
- Set appropriate timeouts: Match timeout to expected operation duration
- Use shorter timeouts for public APIs: Prevent abuse and resource exhaustion
- Longer timeouts for internal services: Trusted services may need more time
- Log timeouts: Track which requests are timing out for debugging
- Consider client expectations: Some clients may have their own timeouts
- Combine with rate limiting: Protect against slowloris attacks
- Graceful degradation: Return meaningful error messages on timeout
- No timeout for WebSocket: Long-lived connections shouldn't have request timeouts
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.