Why Fulmine
There are several fast HTTP servers for Node built on µWebSockets.js. What is scarce is one you can drop into an existing Express application without rewriting it. That is the whole project: an Express 5 that runs on µWebSockets.js, and everything else follows from taking both halves of that sentence seriously.
Compatibility is a test suite, not a claim
Every test in the repository runs against real Express first and then against Fulmine, and the two outputs have to match byte for byte: status, headers, body. That is what makes helmet, cors, passport, morgan, multer, express-session and the rest of the ecosystem work rather than "mostly work". Express 5's own test suite runs against Fulmine too and passes whole, 1130 passing, 0 failing, at the pinned Express version.
Frameworks are a larger user of the Express surface than any application, so they have a suite of their own: NestJS, Next.js, Astro, SvelteKit, React Router, Apollo Server, tRPC, tsoa and an MCP server, each served twice and compared.
A change that answers differently from Express is a bug here, even when the new answer looks better. The few places where it cannot be helped are written down with the reason.
Where the speed comes from
Express finds a route by walking its stack and testing each layer against the path, on every request. Fulmine hands every route it can to µWS's own router, which matches in C++, and works out at listen() which middlewares stand in front of each one. Arriving at a handler costs no matching at all, and the gap grows with the route table instead of shrinking: a handful of routes measures 1.3x to 4.9x, a thousand measures 7x to 20x.
Most of the rest is work that does not happen. The body is not read unless a handler asks, the headers are not copied out of µWS unless something reads them, the request is not turned into a stream unless something streams it. A handler simple enough to be read at registration time is compiled into a static response and answered by µWS itself, without entering JavaScript.
Where the cost is a shared library, JSON.parse on a big body, zlib, OpenSSL, it is a wash, and the performance page says so rather than publishing those rows as wins.
What you get on top
Express has no answer for these, so Fulmine adds them, all optional:
express({ cluster: "auto" }): one process per core, every one bound to the same port withSO_REUSEPORT, no primary in the path.app.ws(path, behavior): WebSockets served by µWS itself, with anupgrade(req, res)hook that sees the same request your routes see.express.compression(): thecompressionmodule's options and decisions, about 50% more requests per second.express.static(dir, { preCompressed: true }): the.brand.gztwins your build already wrote.express.serverTiming(): aServer-Timingheader saying how the request was routed.express.testing: assert in a test that a route stays on the fast path, so a commit that slows one down fails CI instead of being found weeks later.- TLS through
express({ uwsOptions }), PROXY protocol through one setting.
What it costs
Every one of these is a real price, so here they are:
- A native binary. Node 22, 24 or 26 on Linux, macOS or Windows, x64 or arm64, glibc 2.38 or newer. No Alpine, no Bun.
npx fulmine.js verifychecks a machine and a Dockerfile in thirty seconds, and Deploying has the image that works. - µWebSockets.js is not on npm. It is installed from GitHub, which needs git at install time and one setting under pnpm.
- A few things answer differently, because there is no
node:httpunderneath: Differences from Express. - The major tracks Express, not semver. Fulmine 5 follows Express 5. If you pin, pin the minor.
Who is behind it
Fulmine is a derivative work of Ultimate Express by @dimdenGD, used under the Apache License 2.0, with the full commit history preserved. It targets Express 5 only, is typed, and adds the compiled responses, the cluster, the testing helpers and the tooling above. It is not affiliated with the OpenJS Foundation or the Express.js project.