6500 zombie processes, all from Stalwart

Issue Description

When logging into the Ubuntu server where Stalwart runs as a Docker, I see:
=> There are 6456 zombie processes.

Reproduction Steps

  1. Leave Stalwart 0.16.x running for a few weeks

ps -eo stat=,ppid= | awk '$1 ~ /^Z/ {c[$2]++} END {for (p in c) print c[p], p}' | sort -rn | head

6469 3184418

# ps -o pid,ppid,user,lstart,args -p 3184418

    PID    PPID USER                      STARTED COMMAND
3184418 3184395 2000     Wed Jul 15 08:59:13 2026 /usr/local/bin/stalwart --config /etc/sta...

Relevant Log Output

In the logs, I see lots of

2026-08-10T19:50:29Z ERROR RocksDB error (store.rocksdb-error) reason = "IO error: While open a file for appending: /var/lib/stalwart//000499.log: Too many open files", details = "Failed to lock event.", causedBy = "crates/store/src/dispatch/lookup.rs:382", details = "Failed to lock event.", causedBy = "crates/smtp/src/queue/spool.rs:236"

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

docker stats says:

1fed1df1acbe   stalwart                         0.08%     1.217GiB / 62.62GiB   1.94%     4.32GB / 3.77GB   8.76GB / 11.6GB   6521 

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

Those aren’t processes Stalwart started. There’s exactly one place in the whole codebase that spawns a child (the Sieve exec plugin), and it waits on it properly; nothing in the mail, queue or filtering paths forks at all.

What’s happening is that Stalwart is PID 1 in your container, because the image uses a plain entrypoint with no init process. Anything that gets executed inside the container namespace and then exits is reparented to PID 1, and PID 1 here is an application that never calls wait(), so the entries pile up as zombies with Stalwart listed as the parent. Our own image is a good candidate for the source: it ships a healthcheck that runs a shell and curl every 30 seconds.

Fix is to add --init to your docker run, or init: true under the service if you use compose, then recreate the container. That inserts a proper init as PID 1 which reaps orphans. Existing zombies go away with the restart. This is on us to document, and I’ll get the docker page updated.

The open files error is a separate problem and not caused by the zombies, since those release their descriptors. RocksDB keeps a lot of SST files open, so you want to raise the limit: --ulimit nofile=65535:65535, or the compose equivalent.

If you want to confirm what’s generating them, docker exec <container> ps -eo stat,pid,ppid,comm and look at the zombie entries. Names like curl or sh confirm the healthcheck; anything else points at a monitoring or backup agent.