In Thunderbird Release 153.0.1 I have been getting a lot of “connection reset” errors when loading my calendars, especially when Thunderbird starts. My server is running Stalwart 0.16.15 - but I have not observed the same behaviour when loading the calendar I have on Thundermail (which runs Stalswart 0.15.x).
Could it be a rate-limiting setting in 0.16.x that is not present in 0.15.x? I have tried searching the UI but have not found aything. that appears to be relevant.
This gives good candidates. Let’s also check maxConnections listener HTTP settings, and loitering ban since Thunderbird may hold idle connections that get treated as loitering.
The most likely culprits are Stalwart’s HTTP rate limiting and auto‑ban settings, both of which are configured in JSON, not previously exposed under the old /api/... endpoints.
HTTP rate limiting: authenticated HTTP requests (which includes all CalDAV/WebDAV traffic) are capped by rateLimitAuthenticated on the Http singleton, default 1000 requests/minute. If Thunderbird is doing many quick DAV requests on startup (multiple calendars, PROPFIND/REPORT bursts), it can exceed this and get rejected. See HTTP rate limiting.
Auto-ban / loitering: Stalwart also auto-bans an IP if it opens many connections without sending meaningful traffic (loiterBanRate, default 150 disconnections/day) or if authentication fails too often (authBanRate, default 100/day). If Thunderbird opens many parallel connections at startup for each calendar, it could trip this and get its IP added as a BlockedIp, causing subsequent resets. Check Settings → Security → Blocked IPs in the WebUI to see if your client’s IP shows up there. See Auto-banning.
Connection caps: each NetworkListener (and the server-wide SystemSettings.maxConnections, default 8192) also caps concurrent connections; if your HTTP listener has a lower maxConnections set, many simultaneous DAV connections could be refused. See Listeners.
Since you’re comparing against a 0.15.x instance, it’s worth noting these auto-ban and rate-limit mechanisms and their JSON-based configuration are part of the current settings model, so a default that used to be more lenient (or absent) may now be tripping. I’d first check Settings → Security → Blocked IPs for your own IP, then loosen rateLimitAuthenticated and loiterBanRate/authBanRate if you see hits there.
This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.
There is a per-account concurrent request limit that defaults to 4, and it applies to CalDAV as well as JMAP. Hitting it does not produce a clean 429: the server closes the connection, which the client reports as a connection reset. That fits “especially when Thunderbird starts”, since it opens several calendar collections at once.
Two things to check: maxConcurrentRequests (and maxConcurrentUploads) under the JMAP settings, and the authenticated rate limit under the HTTP settings. You can also exempt the account rather than raising the global limit by granting it the unlimited requests permission. Worth a look at the security settings too, since repeated limit events feed the abuse counter and can escalate to an IP ban, which looks identical from the client side.
To confirm it in one step, look for limit.concurrent-request or limit.too-many-requests events at the moment Thunderbird starts. If you are behind a reverse proxy, check that forwarded headers are enabled as well, otherwise every client shares one apparent IP and those counters trip far sooner than they should.
I have raised the limits you mention - but the connection resets persist. I have 9 calendars. And when I grep the log for limit.concurrent-request or limit.too-many-requests there are no entries. When gepping for my clients IP address, do not get any hits either, however, this seems odd. I have set Services > HTTP > General > Obtain remote IP from Forwarded header to ‘On’ and I have this Nginx server block for the Stalwart domain:
server {
server_name mail.domain.dk;
listen [xxx:xxx:xxx:xxx::1]:443 ssl http2;
listen x.x.x.x:443 ssl http2; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/mail.domain.dk/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/mail.domain.dk/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
error_log /var/log/nginx/mail.domain.dk.error.log;
location / {
proxy_pass http://localhost:8081;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
The only HTTP authentication successes appears to be from 127.0.0.1.
No, it isn’t correct, and I think the block is the whole problem. You have proxy_set_header Connection "upgrade"; unconditionally, so every ordinary CalDAV request goes upstream declaring an upgrade with no Upgrade header to go with it. Connection is a hop-by-hop header, so that also replaces the keep-alive that proxy_http_version 1.1 would otherwise imply, and connection reuse to the upstream dies. Resets clustered at Thunderbird startup, when it opens nine collections at once, is exactly the shape that produces. The standard form is:
You can also add proxy_set_header X-Forwarded-Proto $scheme;.
On the missing IPs: we don’t read X-Real-IP at all. With the forwarded-header option on we look at Forwarded first and fall back to X-Forwarded-For, so your X-Forwarded-For line is the right one and should be working. If you’re still only seeing 127.0.0.1 then that option isn’t actually in effect on the listener handling these requests; grep for http.x-forwarded-missing to confirm. And since no limit.concurrent-request or limit.too-many-requests events appear at all, the concurrency limits are ruled out, which fits: this is happening below the application layer.
The upgrade header was a real problem but clearly not the whole one, so let’s go after it differently. Two things got lost in the rewrite, though, and I’d put them back before anything else: proxy_set_header Host $host; and proxy_http_version 1.1; were both in your earlier config and are absent from the new one. Without the latter nginx talks HTTP/1.0 upstream and there’s no keepalive at all, which is a step backwards from where you started. Also change proxy_pass http://localhost:8081; to 127.0.0.1, so nginx can’t try IPv6 first if Stalwart isn’t bound dual-stack.
The artifact I actually want is Stalwart’s stderr rather than its log. A panic in a connection task closes the socket with nothing written to the Stalwart log at any level, nginx reports it as the upstream closing prematurely, and the client sees a reset. That is precisely your profile: resets, a clean log, and no limit events anywhere. I had another report this week with the identical “no errors anywhere” shape and it turned out to be exactly that. So: put RUST_BACKTRACE=1 in the environment, restart, reproduce with Thunderbird, then grab journalctl -u stalwart -n 200 or docker logs <container> 2>&1 | tail -50 depending on how you run it.
Two smaller things alongside it. The nginx error log for that vhost across a Thunderbird startup will tell us immediately which side is dying: “upstream prematurely closed connection” means the server, an SSL or client error means it never got that far. And I still don’t have the answer on the forwarded headers, so grep for http.x-forwarded-missing; you’re still only seeing 127.0.0.1, which means that setting isn’t taking effect on the listener handling these requests.