Bug Report
Version
├── axum v0.8.7
│ ├── axum-core v0.5.5
Platform
Linux 6.14.0-34-generic #34~24.04.1-Ubuntu
Description
When using graceful_shutdown with TcpListener created with from_raw_fd the shutdown hangs until further requests which fail are made. The issue can also be reproduced with a blocking TcpStream created with from_raw_fd instead of TcpListener directly.
Short reproduction of the bug:
use std::{os::fd::FromRawFd, time::Duration};
use axum::{Router, response::IntoResponse};
use tokio::{net::{TcpListener}, time};
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
async fn timeout() -> () {
time::sleep(Duration::from_secs(15)).await;
println!("Graceful shutdown");
}
#[tokio::main]
async fn main() {
let _log = tracing_subscriber::registry()
.with(EnvFilter::from_default_env().add_directive("trace".parse().unwrap()))
.with(
fmt::layer()
.with_ansi(true)
.with_level(true)
.with_target(true),
)
.try_init();
let listener = unsafe { std::net::TcpListener::from_raw_fd(3) };
let tokio_listener = TcpListener::from_std(listener).unwrap();
let request_handler = async |input: String| input.into_response();
let router = Router::new().fallback(request_handler);
axum::serve(tokio_listener, router)
.with_graceful_shutdown(timeout())
.await
.unwrap();
}
What happens:
$ systemd-socket-activate -l 127.0.0.1:9123 ./target/release/fd_repro
Listening on 127.0.0.1:9123 as 3.
Communication attempt on fd 3.
Execing ./target/release/fd_repro (./target/release/fd_repro)
2025-11-18T11:38:53.378754Z TRACE axum::serve: connection 127.0.0.1:57154 accepted
Graceful shutdown
2025-11-18T11:39:08.379283Z TRACE axum::serve: received graceful shutdown signal. Telling tasks to shutdown
# Hangs here after the shutdown future completes
# The following log is from trying to make a request until the program manages to shut down
2025-11-18T11:39:13.332484Z TRACE axum::serve: connection 127.0.0.1:44526 accepted
2025-11-18T11:39:13.332559Z TRACE axum::serve: signal received in task, starting graceful shutdown
2025-11-18T11:39:13.332572Z TRACE axum::serve: failed to serve connection: Cancelled
...
2025-11-18T11:40:06.390920Z TRACE axum::serve: connection 127.0.0.1:33176 accepted
2025-11-18T11:40:06.390963Z TRACE axum::serve: signal received, not accepting new connections
2025-11-18T11:40:06.390988Z TRACE axum::serve: waiting for 1 task(s) to finish
2025-11-18T11:40:06.390986Z TRACE axum::serve: signal received in task, starting graceful shutdown
2025-11-18T11:40:06.391006Z TRACE axum::serve: failed to serve connection: Cancelled
# program exits here
What should happen:
systemd-socket-activate -l 127.0.0.1:9123 ./target/release/fd_repro
Listening on 127.0.0.1:9123 as 3.
Communication attempt on fd 3.
Execing ./target/release/fd_repro (./target/release/fd_repro)
2025-11-18T11:43:54.102130Z TRACE axum::serve: connection 127.0.0.1:40936 accepted
Graceful shutdown
2025-11-18T11:44:09.103194Z TRACE axum::serve: received graceful shutdown signal. Telling tasks to shutdown
2025-11-18T11:44:09.103306Z TRACE axum::serve: signal received, not accepting new connections
2025-11-18T11:44:09.103368Z TRACE axum::serve: waiting for 0 task(s) to finish
# Program exits here
Another reproduction I managed to make is replacing the TcpListener from the earlier example with
let stream = unsafe { std::net::TcpStream::from_raw_fd(3) };
stream.set_nonblocking(false).unwrap(); // Works as intended when true, hangs when false
let socket = tokio::net::TcpSocket::from_std_stream(stream);
socket.set_nodelay(true).unwrap();
let tokio_listener = socket.listen(1024).unwrap();
Bug Report
Version
├── axum v0.8.7
│ ├── axum-core v0.5.5
Platform
Linux 6.14.0-34-generic #34~24.04.1-Ubuntu
Description
When using graceful_shutdown with TcpListener created with from_raw_fd the shutdown hangs until further requests which fail are made. The issue can also be reproduced with a blocking TcpStream created with from_raw_fd instead of TcpListener directly.
Short reproduction of the bug:
What happens:
What should happen:
Another reproduction I managed to make is replacing the TcpListener from the earlier example with