Summary
While migrating to axum 0.8 + tonic 0.14, I ran into a situation where a non-Send gRPC handler future was effectively hidden when using connectrpc-axum, but became immediately obvious when switching to direct Axum routing with post_service.
This is not a bug in connectrpc-axum, but the abstraction can make the real root cause harder to discover, especially for users migrating to newer Axum versions.
Root cause
The actual issue was in my tonic service implementation.
I was holding a tokio::sync::MutexGuard across an .await, which makes the handler future !Send.
Axum 0.8 correctly requires:
Service::Future: Send
Problematic pattern (old code)
#[derive(Clone)]
pub struct LocalAuth {
issuer: Arc<JwtIssuer>,
dbcache: Arc<tokio::sync::Mutex<DbCacheWrapper>>,
}
#[tonic::async_trait]
impl Auth for LocalAuth {
async fn init(
&self,
req: Request<InitRequest>,
) -> Result<Response<InitResponse>, Status> {
let uuid = req.into_inner().uuid;
// ❌ MutexGuard is held across an await
let client_id = {
let mut dbc = self.dbcache.lock().await;
dbc.get_by_uuid(&uuid).await?.customer_id
};
Ok(Response::new(InitResponse {
/* ... */
}))
}
}
Holding a MutexGuard across .await causes the entire future returned by init() to become non-Send.
Why this was confusing with connectrpc-axum
When the service was wired using:
MakeServiceBuilder::new()
.add_grpc_service(AuthServer::new(local_auth))
.build();
the !Send future did not surface immediately at the integration point.
After switching to direct Axum integration:
Router::new()
.route(
"/auth.v1.Auth/{*wildcard}",
post_service(AuthServer::new(local_auth)),
)
Axum failed fast with a clear and direct error:
error[E0277]: the trait bound ...::Future: Send is not satisfied
This instantly pointed to the real issue.
Why this matters
Axum 0.8 enforces Send very strictly (and correctly)
Holding a MutexGuard across .await is a common async Rust pitfall
With connectrpc-axum, this issue may:
surface later
or appear in a less obvious place
This can make debugging harder for users migrating from older stacks
Suggestions (documentation / diagnostics)
This is not a request to change behavior, just a suggestion to improve developer experience.
Documentation note
It would be very helpful to explicitly mention that:
gRPC handlers must be Send
Holding tokio::sync::MutexGuard across .await will break this
This is especially important with axum 0.8+
Earlier diagnostics (if feasible)
If possible, adding earlier trait bounds or clearer diagnostics around:
Service::Future: Send
could help users discover this issue closer to where services are registered.
Summary
While migrating to axum 0.8 + tonic 0.14, I ran into a situation where a non-Send gRPC handler future was effectively hidden when using connectrpc-axum, but became immediately obvious when switching to direct Axum routing with post_service.
This is not a bug in connectrpc-axum, but the abstraction can make the real root cause harder to discover, especially for users migrating to newer Axum versions.
Root cause
The actual issue was in my tonic service implementation.
I was holding a tokio::sync::MutexGuard across an .await, which makes the handler future !Send.
Axum 0.8 correctly requires:
Service::Future: Send
Problematic pattern (old code)
Holding a MutexGuard across .await causes the entire future returned by init() to become non-Send.
Why this was confusing with connectrpc-axum
When the service was wired using:
the !Send future did not surface immediately at the integration point.
After switching to direct Axum integration:
Axum failed fast with a clear and direct error:
error[E0277]: the trait bound
...::Future: Sendis not satisfiedThis instantly pointed to the real issue.
Why this matters
Axum 0.8 enforces Send very strictly (and correctly)
Holding a MutexGuard across .await is a common async Rust pitfall
With connectrpc-axum, this issue may:
surface later
or appear in a less obvious place
This can make debugging harder for users migrating from older stacks
Suggestions (documentation / diagnostics)
This is not a request to change behavior, just a suggestion to improve developer experience.
Documentation note
It would be very helpful to explicitly mention that:
gRPC handlers must be Send
Holding tokio::sync::MutexGuard across .await will break this
This is especially important with axum 0.8+
Earlier diagnostics (if feasible)
If possible, adding earlier trait bounds or clearer diagnostics around:
Service::Future: Send
could help users discover this issue closer to where services are registered.