We load tested Umami, the whole stack

Umami is the open source analytics tool we use for this site, and it ships as a docker compose stack: the Next.js app plus Postgres. We gave Stresix the GitHub repo and nothing else. It started both services on one fresh server and sent 2,500 simulated users at it. This is what came back.

What the run measured

MeasurementResult
Inputgithub.com/umami-software/umami, its own docker-compose.yml, no changes
Server4 vCPU / 8 GB (Hetzner CX33, shared vCPU) running both services
Load2,500 simulated users over a 10 minute ramp, all on the home page
Healthy up toAbout 500 users and 290 requests per second
p95 latency300 ms under 100 users, about 1 s near 300, 1.76 s from 500 on
Errors25% of requests timed out after 10 s, 42% by the end. No 5xx at all
Server CPUAbout 40% overall, with one Node thread pinned at 100%
Per serviceumami 1.5 cores and 681 MB, Postgres 0.05 cores and 78 MB
VerdictKeep the server; run one app process per core. Projected 610 to 870 rps

What happened

For the first few hundred users everything was fine: p95 around 300 ms, then about a second as the load climbed. Near 500 users the app stopped getting faster. Successful requests stayed flat at roughly 290 per second for the rest of the run, and every extra user past that point waited in line until the 10 second timeout. The app never crashed and never returned a 5xx. It just could not answer more than about 290 requests a second.

What ran out

Not the server. The box sat at about 40% CPU and used 1.4 GB of its 8 GB. Inside the umami container one Node thread was at 100% of a core from the first second to the last. Next.js renders on a single thread per process, so one process could only ever use one of the four cores. Postgres was barely awake: 0.05 cores and 78 MB.

What the report said to do

Run one app process per core on the same server (several umami containers behind a proxy, or a process manager in cluster mode) and rerun. That should lift the ceiling to roughly 610 to 870 requests per second at no extra cost. Moving to 8 or 16 vCPUs without that change would only buy idle cores.

Why this is a useful example

It is the same pattern our own self-test found: the total CPU graph says there is plenty of room, and the app still falls over. You only see the real limit with per-core and per-thread numbers, measured inside each container. Because Stresix ran the whole compose stack, it could also rule out the database instead of guessing.

What this does not tell you

The run only loaded the home page, which renders the login screen. The tracking endpoint that receives page views does different work and hits Postgres on every request, so it needs its own run. App and database shared one server here; a managed database adds a network hop. The load came from a second server in the same datacenter, so these numbers measure capacity, not what a visitor across the world sees.

Frequently asked questions

How many users can a self-hosted Umami handle?

In this run, on a 4 vCPU / 8 GB server with the default single process, about 500 concurrent users on the home page (roughly 290 requests per second) before requests started timing out. The tracking endpoint and your own setup will differ, so test your own deployment.

Do I need a bigger server for Umami?

Not for this limit. The server was 60% idle. One Node process was the ceiling, so more processes on the same server is the fix, and bigger hardware only helps after that.

Can Stresix test a docker compose app like this?

Yes. Point it at a repository with a compose file and it starts every service on one fresh server, sends the load to the service that takes traffic, and reports CPU and memory per service.

Related guides

Stop guessing your specs. Start knowing them.

Point Stresix at a repo or Docker image and get a number you can deploy on, in one run.

Start a load test