Issue Description
Affected: v0.15.4 through v0.16.23, main, v1.0.0.
The HTTPS listener never closes idle HTTP/1.1 connections. Behind a load balancer or NAT that silently drops idle flows (AWS NLB does so after 350 s), the client’s later FIN/RST never reaches Stalwart, so the socket stays ESTABLISHED forever and keeps its maxConnections slot. Once the limit is reached, all new HTTPS connections are refused.
Cause: the http1 builder has no timer:
so hyper discards its default 30 s header_read_timeout, which also bounds keep-alive idle time:
and accepted sockets never get SO_KEEPALIVE
Fix: add .timer(hyper_util::rt::TokioTimer::new()) to the builder, and set TCP keepalive in SocketOpts::apply via socket2::SockRef::set_tcp_keepalive.
Expected Behavior
An idle HTTP connection is closed after a bounded time, like hyper’s default 30 s header read timeout or the idle timeouts on Stalwart’s other listeners, or a dead peer is detected with TCP keepalive. Its maxConnections slot is then released.
Actual Behavior
Idle HTTP connections are never closed by the server. If the peer disappears without its FIN/RST reaching Stalwart, the socket stays ESTABLISHED. These sockets accumulate until the listener’s maxConnections is reached and every new connection hits limit.concurrent-connection.
Reproduction Steps
- Run Stalwart with the default HTTPS listener on 443.
- Open a connection and leave it idle:
openssl s_client -connect <host>:443 -quiet, then send one request or nothing. - Inside the Stalwart host or container, run
ss -tni state established '( sport = :443 )'. The connection is still ESTABLISHED after several minutes; it was never closed after 30 s. - To reproduce the leak: put Stalwart behind a load balancer or NAT with an idle timeout (e.g. an AWS NLB at the 350 s default), let clients keep connections idle past that timeout before closing them, and watch the ESTABLISHED count on :443 grow without bound.
Relevant Log Output
2026-09-25T00:21:06Z WARN Concurrent connection limit reached (limit.concurrent-connection) listenerId = “https”, localPort = 443, remoteIp = 62.###.##.###, remotePort = 58818, limit = 8192
Stalwart Version
v0.16.x
Installation Method
Docker
Database Backend
PostgreSQL
Blob Storage
S3-compatible
Search Engine
PostgreSQL
Directory Backend
OIDC
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
on