---
url: https://fulmine.sndesign.it/real-apps.md
---
# Real applications

A hello world says little about an application with a database, twenty middlewares and a session
check on every request. So Fulmine was put in place of Express in real open source projects, with no
change to their code, and asked two things: do their own tests still pass, and how much CPU do their
own endpoints take. Everything, scripts and raw results, is in
[fulmine-compat](https://github.com/nigrosimone/fulmine-compat).

## Their tests

Each suite runs on the Express the project ships with, on plain Express 5, and on Fulmine. Only a
test that passes on Express and fails on Fulmine would count against it.

| project                                                              | its own Express        | plain Express 5         | Fulmine                 |
| -------------------------------------------------------------------- | ---------------------- | ----------------------- | ----------------------- |
| [Actual Budget](https://github.com/actualbudget/actual), sync server | 595 passed             | 595 passed              | 595 passed              |
| [LibreChat](https://github.com/danny-avila/LibreChat), API           | 4,840 passed, 6 failed | 4,818 passed, 28 failed | 4,842 passed, 4 failed  |
| [n8n](https://github.com/n8n-io/n8n), CLI integration suite          | 5,618 passed, 9 failed | 6,123 passed, 14 failed | 6,109 passed, 10 failed |

No test fails on Fulmine that passes on Express: what fails on Fulmine fails on Express too, on the
machine the suites ran on. One setting was needed, `body methods`, since Express reads a body on any
verb and Fulmine by default only on POST, PUT, PATCH and QUERY; Actual's tests send a body with GET and
DELETE, and LibreChat's DELETE routes take one, see [differences](./differences.md).

## Their endpoints

[LibreChat](https://github.com/danny-avila/LibreChat) and [Actual Budget](https://github.com/actualbudget/actual), under the load of their own web client. n8n is left out here: it builds
its server with `http.createServer(app)`, so without a change Fulmine serves it through node:http,
which is a smaller story.

The same requests their web client sends, one by one, CPU per request against Express:

Where the framework is the work, the health check, the page, a static file, the gain is large. Where
the database is the work it is smaller, and still there.

The best setup moves the project to Fulmine's own compression and static files,
`express.compression()` and `express.static(dir, { preCompressed: true })`, see
[performance](./performance.md#performance-tips):
[five lines in LibreChat](https://github.com/nigrosimone/fulmine-compat/blob/main/patches/librechat.diff),
[one in Actual](https://github.com/nigrosimone/fulmine-compat/blob/main/patches/actual.diff) plus a
build step that writes the brotli and gzip files.

## What it is worth

These servers are light, so it depends on scale. With a million active users a day, on
{{ data.prices.cloud }} with a task of 1 vCPU and {{ data.prices.taskGb }} GB per process at
{{ Math.round(data.prices.utilization \* 100) }}% CPU and ${{ data.prices.egressGb }} per GB out:

Compute is a small part of that bill and outbound traffic the large one. Without a change Fulmine
takes {{ less('actual', 'fulmine') }}% of the compute on Actual and {{ less('librechat', 'fulmine') }}% on LibreChat, which barely shows in the total. With the
best setup most of the saving is the web app going out compressed, which Express can also do through
a plugin: what Fulmine adds is one option, and less CPU per file. A single small instance sees no
difference on the invoice, it pays its minimum either way.

## How it was measured

One ARM core of a GitHub Actions runner ({{ data.machine }}, 4 cores, no SMT) for the server, wrk on two
others, the database on the fourth. Both servers start from one checkout as their Docker image starts
them, each with its own database and the same seed. Five alternating rounds per endpoint, CPU read
from `/proc`, and an A/A run of Express against Express to measure the noise. Each project runs {{ data.runs }} times, each
time on its own machine, and the figures are the median: the runs agree within {{ agree }} points. Both servers run
without V8's memory reducer and get one full GC after seeding, so the pauses between rounds do not
change their heap. Measured on
{{ data.date }}, Express {{ data.projects\[0].express }}, Fulmine {{ \[...new Set(data.projects.map((p) => p.fulmine))].join(' and ') }}. To run
it again, see the [repository](https://github.com/nigrosimone/fulmine-compat).
