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.
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, sync server | 595 passed | 595 passed | 595 passed |
| LibreChat, API | 4,840 passed, 6 failed | 4,818 passed, 28 failed | 4,842 passed, 4 failed |
| 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.
Their endpoints
LibreChat and Actual Budget, 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.
- Express60 ms
- -19%fulmine, no change49 ms
- -19%fulmine, best setup49 ms
- Express63 ms
- -6%fulmine, no change59 ms
- -26%fulmine, best setup46 ms
The same requests their web client sends, one by one, CPU per request against Express:
| endpoint | what it is | no change | best setup |
|---|---|---|---|
| Actual Budget, sync server | |||
health | GET /health, the container health check | 2.94x | 2.98x |
validate | GET /account/validate, the session check on every client start | 1.40x | 1.40x |
list-files | GET /sync/list-user-files, the budget picker | 1.35x | 1.35x |
sync-idle | POST /sync/sync with nothing new, what an open budget sends over and over | 1.12x | 1.12x |
sync-day | POST /sync/sync from a day back, a client catching up | 1.05x | 1.05x |
index | GET /, the web app's index.html | 8.18x | 8.04x |
bundle | GET of the main bundle, 1.2 MB: a first visit, a new release, a browser without cache | 1.46x | 34.60x |
bundle-304 | GET of the main bundle with If-None-Match, a returning browser | 2.23x | 2.02x |
| LibreChat, API server | |||
health | GET /health, the container health check | 3.48x | 3.50x |
index | GET /, the page, with a CSP nonce of its own each time | 1.25x | 2.18x |
bundle | GET of the main bundle, 0.9 MB served from its .gz twin: a first visit, a new release | 1.13x | 10.76x |
config | GET /api/config, the first call of every page load | 1.04x | 1.22x |
user | GET /api/user, the signed in user | 1.09x | 1.28x |
endpoints | GET /api/endpoints, the model menu | 1.08x | 1.25x |
convos | GET /api/convos, the sidebar: 25 conversations of 200 | 1.06x | 1.22x |
messages | GET /api/messages/:id, opening a conversation of 20 messages | 1.06x | 1.24x |
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: five lines in LibreChat, one in Actual 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 AWS Fargate ARM, us-east-1 with a task of 1 vCPU and 2 GB per process at 50% CPU and $0.09 per GB out:
| a year | Express | Fulmine, no change | Fulmine, best setup |
|---|---|---|---|
| Actual Budget, sync server | $18,564 | $18,418 | $12,991, -30% |
| LibreChat, API server | $7,480 | $7,455 | $6,316, -16% |
Compute is a small part of that bill and outbound traffic the large one. Without a change Fulmine takes 19% of the compute on Actual and 6% 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 (Neoverse-N2, 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 3 times, each time on its own machine, and the figures are the median: the runs agree within 2 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 2026-10-28, Express 5.3.0, Fulmine 5.25.12. To run it again, see the repository.