Memory Leak or Memory Exhaustion leading to OOM-Kill

Issue Description

Since upgrading from v0.16.11 to v0.16.22, Stalwart is repeatedly killed by the Linux OOM killer. The kills correlate directly and reproducibly with the trainSpamClassifier scheduled task. This did not occur on v0.16.11 over a period of approximately 2.5 months on the same hardware.

Expected Behavior

no memory exhaustion should happen

Actual Behavior

The Linux OOM killer terminates the Stalwart process within seconds to minutes of the trainSpamClassifier task being scheduled or starting. The server then automatically restarts via systemd.

Relevant Log Output

The issue is fully reproducible and follows this exact pattern, confirmed across multiple kill events on 2026-09-27:

Kill 1 — 07:32:

06:35:22 trainSpamClassifier scheduled for 07:32:34
07:32:34 trainSpamClassifier due
07:32:49 OOM-Kill (kernel log)
07:32:59 Stalwart restarts

Kill 2 — 21:49:

13:47:48 trainSpamClassifier scheduled for 21:57:26
21:48:49 last normal log entry
21:49:36 OOM-Kill (kernel log)
21:49:42 Stalwart restarts
21:49:42 trainSpamClassifier rescheduled for next day

Kernel OOM log at kill time (example):

Sep 27 08:37:03 mail kernel: Out of memory: Killed process 196503 (stalwart)
total-vm:19297084kB, anon-rss:1619280kB, file-rss:2816kB, shmem-rss:0kB,
UID:999 pgtables:19156kB oom_score_adj:0

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

The system has 4 GB swap (nearly empty at kill time — only ~88 MB used), so the issue is not swap exhaustion as in earlier versions
The tokio runtime worker thread invokes the OOM killer: tokio-rt-worker invoked oom-killer

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

This matches a known bug that was fixed after the version you’re on. In 0.16.24, the changelog lists this fix under Spam filter:

“Incremental training never advances its position past the first run, so every retained sample added since then is trained again, and counted again in the reservoir, on each run until it expires.”

That means on 0.16.22 (and earlier 0.16.x releases affected by this regression), each scheduled trainSpamClassifier run re-processes and re-loads into memory the entire growing set of retained training samples instead of only the new ones since the last run, so the reservoir and in-memory sample set keep growing over time until a run blows past available RAM. That lines up with your observation: it worked fine for 2.5 months (fewer accumulated samples) and started OOM-killing once the retained sample set grew large enough, and the kill correlates exactly with the training task.

Upgrading to 0.16.24 (or later) should resolve this, since that release specifically fixes this incremental-training bug. You can also work around it in the interim by triggering a full retrain (which rebuilds from the reservoir and resets the last_id position) or by lowering holdSamplesFor/reservoirCapacity on the SpamClassifier object to shrink the retained sample set, but the proper fix is the version upgrade.

Reference: server CHANGELOG.md, “0.16.24” Fixed > Spam filter section.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Before anything else I’d upgrade to 0.16.24. It fixes a bug where incremental training never moved past the position of its first run, so every scheduled run since then has been processing the whole retained sample history, 180 days by default. That’s the change in training workload I can point to between your versions; I can’t yet show it’s what pushes the process to 1.6 GB. The position is only saved when a run completes, so the first run after the upgrade still starts from the old one and could hit the same wall. Lowering holdSamplesFor before you upgrade makes that run smaller, and clearing trainFrequency stops scheduled training altogether if you need the box stable while we look (Training | Stalwart).

If it still gets killed on 0.16.24, send the spam.train-started and spam.train-completed lines from before a kill (the Total field matters), any other spam.train events right before it, and how much RAM the machine has. Have any unusually large messages been moved into or out of Junk recently?