Deadlines and cancellation
Use the normal message's single time budget, cooperative cancellation, and independent cleanup authority correctly.
A message's normal pipeline uses one absolute deadline. Cancellation requests that processing stop; it does not alone prove that the future or a side effect in an external system has definitely finished.
Choose the duration#
Action #[timeout(seconds = N)]
→ controller #[timeout(seconds = N)]
→ ServerConfig::message_timeout_secsThe normal timeout is 1..=300 seconds. Before middleware, all guards, extractors, the action, response preparation, and normal reverse middleware share this budget. Each phase does not receive a fresh full duration.
Observe the signal#
#[message("bounded")]
#[timeout(seconds = 2)]
async fn bounded(&self, stopping: ExecutionCancellation, deadline: MessageDeadline)
-> Result<NoReply, WebSocketActionError>
{
let work = tokio::time::sleep(std::time::Duration::from_millis(10));
let _remaining = deadline.remaining();
tokio::select! {
biased;
_ = stopping.cancelled() => Ok(NoReply),
_ = work => Ok(NoReply),
}
}The complete controller file includes the imports. ExecutionCancellation is observe-only; it provides no .cancel(), raw token, child token, or .0 access. If an external operation requires its own token, build the explicit bridge in the cache/Mongo example.
Cooperative window#
A local timeout triggers the execution signal. The framework continues polling the normal pipeline for a cooperative window of at most 250 ms; the shutdown root bound can shorten it. A new cancellation reason does not restart the window.
If the pipeline returns an actual result during this window, success or the application error is preserved. Receiving a signal does not automatically turn the result into a timeout. Otherwise, the execution slot is stopped while cleanup and scope completion ownership continue. On a healthy connection, the next message can be processed after the required evidence is complete; the typical local error is keep-open MESSAGE_TIMEOUT.
Independent budgets#
| Field | Default | Scope |
|---|---|---|
message_timeout_secs |
30 s | Entire normal message pipeline |
message_cleanup_timeout_secs |
10 s | Aggregate termination exits |
connection_middleware_timeout_secs |
10 s | Handshake/identity/admit/opened invocations, aggregate closed |
connection_lifecycle_timeout_secs |
30 s | Default connected/disconnected invocation duration |
handshake_timeout_secs |
10 s | Upgrade boundary |
write_timeout_millis |
5000 ms | Write/flush; shared terminal/Close drain cap during shutdown |
outbound_admission_timeout_millis |
5000 ms | Waiting for queue capacity |
Inbound waiting, scope disposal, and transport work are not added as new phases to the normal message deadline. Cleanup receives its own CleanupCancellation; do not pass normal execution's signal into cleanup.
Guarantee boundaries#
Normal shutdown stops new admission; it does not immediately cancel an already selected action merely because draining began. Execution-owned send authority is removed at the cooperative execution cutoff, not immediately on the cancellation signal.
Tokio cannot interrupt a synchronously blocking poll, codec, or destructor midway. A database commit or partial broadcast is not considered rolled back by cancellation. In particular, PostgreSQL's cancellation type differs from WebSocket's; the HTTP example's Some(cancellation) does not compile here.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.