Cookbook / Production
Handler timeouts
Cap how long any route may take, override it per route, and answer 503 instead of hanging.
A slow database, a stuck vendor call or a runaway loop should not tie up a request forever. handler_timeout sets a default limit for every route; a handler that runs over is cancelled at its next .await and the client gets 503 Service Unavailable.
use axeon::Server;
use std::time::Duration;
fn main() {
let mut app = Server::new();
// The default for every route. Off unless you set it.
app.handler_timeout(Duration::from_secs(15));
// Tighter for a hot read path...
app.route("/v1/products")
.timeout(Duration::from_secs(2))
.get(|_req| async { "[]" });
// ...looser for slow work such as large uploads...
app.route("/documents")
.stream_body()
.timeout(Duration::from_secs(300))
.post(|_req| async { "stored" });
// ...and none at all for a long-running stream.
app.route("/assistant").no_timeout().post(|_req| async { "streamed" });
// Takes 5 seconds but is allowed only 1.
app.route("/slow")
.timeout(Duration::from_secs(1))
.get(|_req| async {
tokio::time::sleep(Duration::from_secs(5)).await;
"too late"
});
app.listen("127.0.0.1:3000").expect("server failed to start");
}
curl -i http://127.0.0.1:3000/slow # 503 after one second
Notes#
- The limit covers the route's middleware and handler together. Global middleware (logging, metrics, request IDs) runs outside it, so it still sees and records the
503. - A cancelled handler stops at its next
.awaitand everything it holds is dropped, so an open database transaction rolls back. Work that must finish regardless belongs in a spawned task or a background job. - A streamed response only has to start in time. Once the first bytes are sent, the stream may run as long as it needs.
- Timeouts are kept when you
mounta router, andon_errorseesServerError::HandlerTimeoutlike any other error. - This differs from
request_timeout, which bounds how long a slow *client* may take to send its request.