Sizing¶
This page helps you choose how big a server to give ihasmail. For most sites the answer is short: 1 CPU and 256–512 MiB of memory is enough. Read on if you expect hundreds or thousands of people signed in at once.
Everything here is for ihasmail on its own. Stalwart needs its own, much larger server budget, and nothing here assumes the two share a machine.
How big a server do I need?¶
| Minimum | Comfortable | Why | |
|---|---|---|---|
| CPU | 1 core, x86-64 or arm64 | 2 cores | It passes JMAP requests to Stalwart and serves the app's files. HTTPS is the reverse proxy's work, not ihasmail's. One core is fully busy at roughly 2,000–2,800 JMAP operations a second — on a 1 vCPU host that is the limit you reach first, not memory |
| RAM | 256 MiB | 512 MiB | An idle container measures about 75 MiB in memory, of which ~26 MiB is charged to its cgroup (the container's memory budget). The rest is room for signed-in sessions — how many |
| Disk | 1 GB | 3 GB | The image is ~60 MB to download and about 240 MB unpacked. The deploy script keeps IHASMAIL_KEEP_VERSIONS of them — three — so a roll-back is a docker run rather than a rebuild |
| Storage for mail | none | none | There is no database and no mail store. Everything durable lives on Stalwart, including each user's settings |
A 1 vCPU / 1 GB server is a reasonable whole machine for ihasmail and its reverse proxy together. That is the point of the design, not a coincidence.
Big mailboxes do not need a bigger server. ihasmail caches nothing. Uploads and downloads pass straight through with a size cap, so a 50 MiB attachment does not become 50 MiB of memory.
What memory does grow with is how many people are signed in at once. Each open tab also holds a live-update connection (Server-Sent Events), with a matching connection from ihasmail to Stalwart behind it. So a very large site runs into connection and open-file limits before it runs out of memory.
The quick start needs no disk volume at all. The
alternative mounts one for sessions.json, which is kilobytes per signed-in
session and does not grow with anything else.
How many people at once?¶
Plan for about half of what a server can hold. These are the numbers to plan with:
| Memory | Route to Stalwart | Plan for | Move up when |
|---|---|---|---|
| 256 MiB | https | ~800 signed-in tabs | memory in use settles above ~180 MiB |
| 256 MiB | private http | ~2,500 signed-in tabs | memory in use settles above ~180 MiB |
| 512 MiB | https | ~1,700 signed-in tabs | memory in use settles above ~380 MiB |
| 512 MiB | private http | ~5,000 signed-in tabs | memory in use settles above ~380 MiB |
These count tabs, not people. Someone with the app open on a phone and a desktop is two tabs, and a tab left open overnight still counts. Sessions expire on their own, and an immutable instance drops all of them on every deploy. So the number to size for is your busiest moment, not your total users.
Memory runs out first, and it is used by people signed in, not by mail. What each tab costs depends on one thing you choose: the route to Stalwart.
What one server can hold before it fails¶
These are the ceilings, measured until the container ran out of memory:
STALWART_URL |
Per tab | Tabs in 256 MiB | Tabs in 512 MiB |
|---|---|---|---|
https://… |
~133 KiB | 1,650 | 3,400 |
http://… on a private network |
~48 KiB | 5,000 | ~10,000 |
http://… private, with push by subscription |
~33 KiB | 6,100 | 14,900 |
They were measured against a real Stalwart 0.16.20, not the mock. Signed-in tabs were added 200 at a time until the kernel killed the container. The 512 MiB plain-HTTP figure is worked out from the measured slope; the others were run all the way to failure. The 512 MiB push figure needs one setting, in the note below.
Why plan for half?
The measured tabs sat idle once signed in. Real tabs make requests, and a request uses memory while it is in flight. And the last few hundred tabs before a ceiling are the difference between a slow morning and a container the kernel kills at 09:05.
ihasmail gives no early warning. Its memory climbs in a straight line to the cap, and it answers health checks in a millisecond right up to the step that kills it. Watching memory use is the only warning you get.
Why https costs so much more per tab
Over HTTPS, about 80 KiB of every tab is the encryption library's per-connection state on the link to Stalwart. That memory sits outside Node's own, and no heap or buffer setting touches it.
When Stalwart and ihasmail share a private network — the same host, the same Docker network, a private cloud network — that link can be plain HTTP, and the cost goes away. Read the route to Stalwart before choosing: it is a security decision as well as a sizing one.
--max-old-space-size: only for push by subscription at 512 MiB
Node keeps JavaScript objects in an area called the heap, which has a size
limit of its own. NODE_OPTIONS=--max-old-space-size=… raises it.
On the ordinary relay, leave it alone. Raising the limit made no difference against a real server: 3,400 tabs with it, 3,400 without. What fills the container is connection state, which no heap limit can reclaim.
Push by subscription is different. With no connection to Stalwart per
tab, what a tab costs is mostly JavaScript objects. At 512 MiB the process
reaches Node's default heap limit — about 260 MB on a small host — before
it reaches the container's memory cap. Measured, it died at 10,800 tabs with
a heap out-of-memory error and 140 MB of the container still free. With
NODE_OPTIONS=--max-old-space-size=400, about three quarters of the cap,
the same test ran to 14,975 tabs and was stopped by the memory cap. At
256 MiB the default limit is never reached, so the setting only matters on
the larger size.
Stalwart's own limits¶
Two limits belong to Stalwart, not ihasmail:
- Its listeners accept 8,192 connections at once each by default
(
maxConnections, set per listener). - Its HTTP rate limiter allows 1,000 requests a minute per signed-in account.
ihasmail holds one Stalwart connection per tab. So once you get near either figure, the mail server decides your size, whatever the tables above say.
How these numbers were measured
Measured on the image built from the repo Dockerfile on node:22-alpine,
run --read-only with no volume, against a real Stalwart 0.16.20 on a
private Docker network. Memory was read inside the container (VmRSS and
process.memoryUsage()) and outside it (the cgroup's memory.current), at
every step of 200 signed-in tabs. Each tab was a sign-in followed by an open
/api/events stream, added until the container stopped answering.
LOGIN_RATE_LIMIT and Stalwart's own connection and rate limits were raised
for the runs; nothing else was changed. Every run ended on memory, with
exit code 137 (killed for running out of memory). None ran out of CPU.
Idle tabs are not working ones, which is why the advice above halves the measured ceilings.
What a source install can hold¶
If you run ihasmail from source, use Node 22. It holds as many tabs as the container. Node 26 is about a fifth faster, but holds a quarter to a third fewer tabs in the same memory.
| Runs as | Route | Per tab | Tabs in 256 MiB | Throughput |
|---|---|---|---|---|
| Node 22, source | http://… private, relay |
~47 KiB | 4,700 | 3,160 ops/s |
| Node 22, source | http://… private, push by subscription |
~33 KiB | 5,700 | — |
| Node 26, source | http://… private, relay |
~64 KiB | 3,400 | 3,770 ops/s |
| Node 26, source | http://… private, push by subscription |
~49 KiB | 4,500 | — |
| Container (Node 22) | http://… private, relay |
~48 KiB | 5,000 | 2,290 ops/s |
Node 22 from source holds what the container holds, within about six per cent, so the figures above apply to it as they stand. Node 26 costs about 15 KiB more per tab in both modes, and is about a fifth faster at handling requests.
- Choose 22 when memory is the tighter budget — which it is at every size on this page.
- Choose 26 when the server has memory to spare and request speed matters more than how many people fit.
How these numbers were measured
The same test as the container table: a real
Stalwart 0.16.20, with signed-in tabs added 200 at a time until the process
was killed. Here npm start ran in a cgroup capped at 256 MiB, on both Node
versions a source install is likely to have. The container row is the same
build on the same route, for comparison.
Throughput is 50 clients at once on the same host, with no CPU cap. A shared vCPU will be lower, and the container row pays for its bridge network.