Skip to content

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.

projectits own Expressplain Express 5Fulmine
Actual Budget, sync server595 passed595 passed595 passed
LibreChat, API4,840 passed, 6 failed4,818 passed, 28 failed4,842 passed, 4 failed
n8n, CLI integration suite5,618 passed, 9 failed6,123 passed, 14 failed6,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.

Actual Budget, sync server

  • Express60 ms
  • -19%fulmine, no change49 ms
  • -19%fulmine, best setup49 ms

LibreChat, API server

  • Express63 ms
  • -6%fulmine, no change59 ms
  • -26%fulmine, best setup46 ms
CPU time one active user costs the server in a day: the page, the API calls, the syncs, a new release now and then. Their own endpoints with Chrome's headers, one ARM core, Node 24. The noise of the machine is under 6%.

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

endpointwhat it isno changebest setup
Actual Budget, sync server
healthGET /health, the container health check2.94x2.98x
validateGET /account/validate, the session check on every client start1.40x1.40x
list-filesGET /sync/list-user-files, the budget picker1.35x1.35x
sync-idlePOST /sync/sync with nothing new, what an open budget sends over and over1.12x1.12x
sync-dayPOST /sync/sync from a day back, a client catching up1.05x1.05x
indexGET /, the web app's index.html8.18x8.04x
bundleGET of the main bundle, 1.2 MB: a first visit, a new release, a browser without cache1.46x34.60x
bundle-304GET of the main bundle with If-None-Match, a returning browser2.23x2.02x
LibreChat, API server
healthGET /health, the container health check3.48x3.50x
indexGET /, the page, with a CSP nonce of its own each time1.25x2.18x
bundleGET of the main bundle, 0.9 MB served from its .gz twin: a first visit, a new release1.13x10.76x
configGET /api/config, the first call of every page load1.04x1.22x
userGET /api/user, the signed in user1.09x1.28x
endpointsGET /api/endpoints, the model menu1.08x1.25x
convosGET /api/convos, the sidebar: 25 conversations of 2001.06x1.22x
messagesGET /api/messages/:id, opening a conversation of 20 messages1.06x1.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 yearExpressFulmine, no changeFulmine, 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.

Apache-2.0. Attribution. Not affiliated with the Express.js project.