Zulip RAM Reduction Report
System: atlas
Assessment date: 2026-08-28
Workload: Private, single-user Zulip server used primarily by OpenClaw agents
Zulip version: 12.1 (Docker)
Executive summary
The Zulip application container was using approximately 4.37 GiB of RAM. This is not evidence of a memory leak: it is the expected consequence of Zulip automatically sizing its process model against the host's full 16 GiB of RAM.
The container has no Docker memory limit, so Zulip selected a production-oriented, higher-throughput configuration consisting of eight Django/uWSGI workers and separate Python processes for its background queues. That configuration is unnecessarily large for a private, single-user deployment.
The recommended change is to use three uWSGI workers and Zulip's multithreaded queue-worker mode. This should preserve all required functionality while reducing the application container to an estimated 2–2.5 GiB after warm-up, saving approximately 2 GiB. A host RAM upgrade is not warranted for this workload.
Host memory condition
At the time of assessment:
| Metric | Observed | Evaluation |
|---|---|---|
| Installed RAM | 16.0 GiB | Adequate |
| Used RAM | 7.8 GiB | About 49% |
| Available RAM | 7.9 GiB | Healthy headroom |
| Buffers/cache | 6.4 GiB | Mostly reclaimable |
| Swap | 1.16 GiB / 4 GiB | Allocated, but inactive |
| Swap I/O | 0 in / 0 out | No thrashing |
| Memory PSI | 0.00% | No current memory stalls |
There was a global out-of-memory event on 2026-08-13 involving an unrelated Codex process that grew to roughly 10.7 GiB RSS. The current measurements do not show ongoing host memory pressure.
Zulip memory findings
Docker reported:
zulip-zulip-1 4.373 GiB / 15.62 GiB 27.99%
The container's cgroup memory composition was approximately:
| Category | Memory |
|---|---|
| Anonymous/application memory | 4,208.5 MiB |
| File cache | 652.9 MiB |
| Kernel memory | 78.3 MiB |
| Page tables | 18.9 MiB |
| Reclaimable slab | 47.8 MiB |
| Non-reclaimable slab | 9.7 MiB |
Process RSS cannot be added directly without overcounting shared pages, but it identifies where the fixed overhead originates:
| Component | Process layout | Approximate summed RSS |
|---|---|---|
| Django/uWSGI | 8 workers | 2.40 GiB |
| Queue processors | 13 separate Python workers | 2.37 GiB |
| Email and scheduled-message services | 3 processes | 499 MiB |
| Tornado event server | 1 process | 217 MiB |
| KaTeX server | 1 process | 81 MiB |
| Full-text indexing | 1 process | 51 MiB |
| Upload, proxy, nginx, and supervision | Several processes | About 110 MiB |
The queue workers and web workers load substantial parts of the Python/Django application even when idle. Consequently, Zulip's baseline memory consumption depends more on process count than on the number of active users.
Root cause
The effective uWSGI configuration contained:
processes=8
The Zulip container had no Docker memory limit or reservation. It therefore saw the host's entire 16 GiB memory capacity and automatically chose:
- Eight Django/uWSGI web workers.
- Higher-throughput multiprocess queue handling, with a separate Python process for each queue.
Official Zulip documentation explains that:
- uWSGI worker count scales automatically from three workers on small hosts to sixteen on hosts with more than 24 GiB;
- queue workers automatically use multiprocess mode when enough memory is detected;
- an unlimited Docker container may see host memory and select the heavier mode; and
- multithreaded queue mode can save approximately 1.5 GiB.
This makes the observed footprint normal for the selected configuration but inefficient for this particular workload.
Recommended configuration
Add the following to the zulip service in the deployment's Compose override:
services:
zulip:
environment:
CONFIG_application_server__queue_workers_multiprocess: "false"
CONFIG_application_server__uwsgi_processes: "3"
Recreate the Zulip application container through Docker Compose for the generated configuration to be updated.
Why these values
- Three uWSGI workers is Zulip's low-memory baseline and is ample for one user plus low-volume agent traffic.
- Multithreaded queue workers retain the queue functions while trading unused high-volume throughput for lower memory consumption.
- An explicit configuration is preferable to relying on automatic detection because the container currently sees the much larger host memory allocation.
Expected outcome
| Change | Estimated saving |
|---|---|
| Multiprocess to multithreaded queues | 1–1.5 GiB |
| Eight to three uWSGI workers | Approximately 1–1.5 GiB |
| Combined practical saving | Approximately 2 GiB |
Expected steady-state footprint after warm-up:
- Zulip application container: approximately 2–2.5 GiB
- Complete Zulip stack, including PostgreSQL, RabbitMQ, Redis, and Memcached: approximately 2.5–3 GiB
These are estimates. Actual savings should be verified after the new container has warmed up and processed normal OpenClaw traffic.
Optional secondary optimization
The separate KaTeX service uses roughly 80 MiB and can be disabled if mathematical notation is never used. This saving is modest, and Zulip warns that disabling the service makes complex math rendering substantially slower. It is not part of the primary recommendation.
Validation plan
After applying the configuration:
- Confirm that the Zulip application starts successfully and OpenClaw can send and receive messages.
- Confirm that the generated uWSGI configuration reports three processes.
- Confirm that queue processing is operating in multithreaded mode.
- Record
docker stats --no-streamafter startup and again after 24 hours of normal use. - Check RabbitMQ for queue backlogs.
- Check the kernel journal for new OOM events.
- Retain the reduced settings if the application container remains near 2–2.5 GiB without queue accumulation or user-visible latency.
Rollback
If queue backlogs or response-time problems appear, remove the two explicit environment variables and recreate the container. Zulip will return to automatic sizing. A more conservative intermediate option is to retain multithreaded queues while increasing uWSGI workers from three to four.
Final evaluation
The current 4.37 GiB consumption is normal but oversized, not a leak. Zulip selected a multi-user production process layout because Docker exposed the host's full memory capacity. For this private deployment, explicit low-memory settings are appropriate and should recover roughly 2 GiB without compromising normal operation. No RAM upgrade is needed.