Issue Description
Stalwart 1.0.3 (8b32686), 3 nodes, PostgreSQL + Patroni through a local HAProxy, Valkey, S3 blobs.
The remote answers 250 and Stalwart logs delivery.delivered. The write that should remove the message from the queue fails on a database error. It’s logged and not retried. The lock is released and the message is still due, so a later queue scan sends it again. The node is alive throughout. The remote gets the message twice.
Expected Behavior
A delivered message isn’t sent again when the final queue save fails: the save retries, keeping the lock, for long enough to cover a database failover.
Actual Behavior
Result, 2 runs, recipients counted at the receiver, each run the same:
| Messages | Delivered twice | |
|---|---|---|
| Control: same hold, about 40 s, database up | 3 per run | 0 |
| Database refused when the 250 arrives | 5 per run | 5 per run |
| Same, 1 message with 2 recipients | 2 recipients per run | 2 per run |
Resends came 80 to 140 s after the 250, from the same node. Nothing was lost.
Reproduction Steps
- Run an SMTP receiver that accepts the message but holds its reply after the final dot.
- Submit a message and wait until the receiver has it.
- Make the database unreachable from the node. We refused 127.0.0.1:5433 with
nftand closed open sockets withss -K. PostgreSQL itself kept running. - Have the receiver answer 250.
- Restore database access (we used 75 s).
Relevant Log Output
Log, one message (shortened):
02:18:37 queue.authenticated-message-queued queueId=338826606726351360
02:19:14 delivery.delivered code=250 elapsed=36861ms
02:19:14 store.postgresql-error reason="error connecting to server: Connection refused (os error 111)"
details="Failed to write to update queue." causedBy=crates/smtp/src/queue/spool.rs:1005
02:20:43 delivery.delivered code=250 elapsed=476ms
Stalwart Version
v1.x
Installation Method
Binary (Linux)
Database Backend
PostgreSQL
Blob Storage
S3-compatible
Search Engine
PostgreSQL
Directory Backend
Internal
Additional Context
Source as I read it:
crates/smtp/src/outbound/delivery.rs:1664-1683:save_changesandremovereturn a bool, and it’s ignored.removestill returnsQueueEventStatus::Completed.crates/smtp/src/queue/spool.rs:1001-1010: on error, log and return false. One attempt.delivery.rs:109: the lock is released after the attempt either way.
Same fault without any block, PostgreSQL primary powered off under outbound load: 470 messages, 3 delivered twice. Delivered +2 to +3 s, spool.rs:1005 “connection closed” +13 s, delivered again +34 s, same node.
Any write error at that point should do the same: I also saw it with cannot execute DELETE in a read-only transaction (25006) at spool.rs:1005, when a pooled connection was on a standby.
I’m not asking about a node that dies right after the 250; I know that can’t be closed completely. Here the node stays up and knows the save failed.
Related, cause looks different: Redis lock handoff reuses a FoundationDB read version and leaves negative queue-quota counters also reports duplicates on v1.0.3, from a stale FoundationDB read after the delete committed. Here the delete fails, on PostgreSQL.
Question: could that final save retry for long enough to cover a database failover, keeping the lock while it does? Is there a setting I’ve missed?
Tested and written with AI help.
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