# ultimate-express reimplements Express 4 on top of uWebSockets

> ultimate-express is an Apache-2.0 JavaScript re-implementation of Express 4 that runs on the uWebSockets.js native binding, with an Express-compatible API and a test suite that diffs behaviour against real Express. It suits Node services that want Express semantics without Express's request handling cost, and it changes a few defaults you have to know about.

**dimdenGD/ultimate-express** — The Ultimate Express. Fastest http server with full Express compatibility, based on µWebSockets.

- Repository: https://github.com/dimdenGD/ultimate-express
- Stars: 1,108 · Forks: 44
- Language: JavaScript
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/dimdengd-ultimate-express

## It is a re-implementation, not a fork, and the tests diff against Express

ultimate-express calls itself the Ultimate Express, a very fast re-implementation of Express.js 4 based on uWebSockets. The README is explicit that it is not a fork of Express.js, which is a meaningful distinction for a compatibility claim.

A fork inherits Express's code and therefore its behaviour, including the parts you did not want. A re-implementation has to re-derive the behaviour, and then it has to prove the derivation is right. The project describes how it does that: to make sure µExpress matches the behaviour of Express in all cases, it runs all tests with Express first and then with µExpress, and compares the results to make sure they match.

That test methodology is the interesting part of the project, more than the speed claim. A compatibility layer for a framework as sprawling as Express is only as good as its test coverage of the corner cases, and diffing against the original is the strongest available approach short of using the original.

The install story is the shortest possible: npm install ultimate-express, then replace express with ultimate-express. The code is plain JavaScript, the main entry is src/index.js, the type definitions are in src/types.d.ts, and the npm package requires Node.js 22 or newer. Older Node versions are still reachable through tagged installs, with ultimate-express@2.1.0 as the last version supporting Node 21, 20 and 19, and ultimate-express@node-v18 for Node 18.

## The benchmark table in the README claims 3x to 11x speedups

The README publishes a comparison table against Express, and the numbers are worth reading with the same care as any vendor benchmark. The stated conditions are wrk with the flags -d 60 -t 1 -c 200, on Ubuntu 22.04 with Node.js 20.17.0, an AMD Ryzen 5 3600 and 64GB of RAM.

Under those conditions, the table reports Express at 11.16k requests per second and µExpress at 75.14k for a simple route, with a 6.73X speedup. A route with many routes registered, /999, shows 4.63k against 54.57k, which is the largest multiplier in the table at 11.78X. A route behind some middlewares shows 10.12k against 61.92k, nested routers 10.18k against 51.15k, and express-static serving 6.58k against 32.45k.

The pattern in the numbers is more informative than the headline figure. The two Express routes that fall furthest behind are the ones where the cost is per-request overhead rather than per-route work: the lot-of-routes case and the static file case. That is consistent with the mechanism, since uWebSockets handles routing and file serving in native code rather than through the Node event loop.

There is also a claim about a real application, a site with templates, static files and dynamic pages with data from a database, where the README reports a 1.5 to 4X speedup in requests per second depending on the page. A database-backed page is a more honest benchmark than a hello world, because it shows the framework overhead in proportion to the rest of the request.

The honest caveat is that these are the project's own numbers, produced with a specific tool, a specific concurrency level and a specific machine. Your own workload will differ, and the last section of this article covers when the gap closes.

## The optimisations only apply to plain string paths with case sensitivity on

The performance tips section is where the honest limits are, and the first one matters most. µExpress tries to optimise routing as much as possible, but it is only possible if case sensitive routing is enabled, which it is by default unlike in normal Express, and only string paths without regex characters like *, +, (), {} and so on can be optimised. Optimised routes can be up to 10 times faster than normal routes, as they use the native uWS router and have a pre-calculated path.

So the speedup is conditional on your route table. An application whose routes are full of parameterised patterns, which is common in REST APIs, will get less of it. The one-line router case gets the most.

The remaining tips are about avoiding the layers that defeat the native path. Do not use the external serve-static module, because the built-in express.static() middleware is optimised for µExpress. Do not use body-parser, because the built-in express.text() and express.json() are the optimised ones. And do not set body methods to read the body of GET requests or other methods that do not need one, because reading a body makes the endpoint about 15% slower.

That last one is why the body methods default is POST, PUT and PATCH only. It is a performance default, and it is also a behavioural change from Express, where express.json() applies to any request with a body.

The last tip concerns a thread pool. By default µExpress creates one child thread, or zero if the CPU has only one core, to improve the performance of reading files. You change the number by setting threads in express(), and set it to 0 to disable the thread pool entirely.

## HTTPS moves into uwsOptions, and headers cap at 4096 bytes by default

Two changes will break a port if you do not know about them.

The first is TLS. In Express you would create an https server and pass the app to it, reading the key and certificate yourself. With µExpress you instead pass uwsOptions to the express() constructor, and the app is created inside the library:

```js
const express = require("ultimate-express");

const app = express({
    uwsOptions: {
        // https://unetworking.github.io/uWebSockets.js/generated/interfaces/AppOptions.html
        key_file_name: 'path/to/key.pem',
        cert_file_name: 'path/to/cert.pem'
    }
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});
```

The README also says this applies to non-SSL HTTP: do not create an http server manually, use app.listen() instead. The reason is structural. uWebSockets.js owns the listening socket, so handing it to a separately created server is not possible in the Express style, and the options have to travel through the constructor instead.

The second is the header size limit. Node.js allows a maximum header size of 16384 bytes, while uWebSockets defaults to 4096 bytes. If you need longer headers, the README says to set the environment variable UWS_HTTP_MAX_HEADERS_SIZE to the max byte count you need.

This is the kind of limit that does not show up in a smoke test. An application that authenticates with a large token in a header, or sets a long cookie, works on Express and fails with a malformed request on µExpress. A port that passes the happy path and fails in production is the failure mode to guard against, and the fix is one environment variable.

## The dependency list is Express's own middleware, vendored into the package

The package.json dependency list is the clearest statement of the approach, and it is a long list of Express's own building blocks: accepts, cookie, cookie-signature, content-disposition, encodeurl, etag, fresh, mime-types, ms, proxy-addr, qs, range-parser, statuses, type-is and vary. There is also acorn for parsing, bytes for size formatting, fast-querystring, fast-zlib, tseep, and the @types/express package for the type definitions.

Alongside them sits the engine, uWebSockets.js, pinned to the GitHub commit reference github:uNetworking/uWebSockets.js#v20.70.0. That is a native addon, so the package pulls a prebuilt or compiled binary rather than a pure JavaScript implementation.

The design decision here is deliberate. Rather than reimplementing the middleware ecosystem, µExpress depends on the same modules Express depends on, and reimplements the parts where Express's own overhead lives, which is the request and response handling and the router. That is why the compatibility claim is credible, and it is also why the package is a large dependency tree for what looks like a small library.

The native dependency is the other cost. uWebSockets.js has to be installed as a compiled binary for your platform and Node ABI, which rules out some deployment targets that a pure JavaScript Express app would support, and it is the reason the project tells you to install an older tagged version if you are still on Node 18, 20 or 21.

The test tooling is minimal and well chosen. The test script is node tests/index.js, type tests run through tsd with the tsd --files tests/types/*.test-d.ts invocation, coverage uses nyc with an HTML report, and the benchmark comparison has its own script at benchmark/run.js.

## It competes with Bun, hyper-express and uwebsockets-express

The README has a section comparing similar projects based on uWebSockets, and the differences are about drop-in fidelity rather than raw speed.

Express on Bun is described as about two to three times faster than on Node.js, because Bun uses uWS for its HTTP module, but still almost two times slower than µExpress because it does not do uWS-specific optimisations. The comparison here is structural: the same uWebSockets engine underneath, but a layer between it and the routing that µExpress does not have.

hyper-express is described as having a similar API to Express while being very far from being a drop-in replacement, because it implements most of the functionality differently. The consequence the README draws is a lot of random quirks and issues that make the switch difficult, and built-in middlewares that are very different, with Express middlewares mostly unsupported.

uwebsockets-express is closer to a drop-in replacement, but the README says it misses a lot of APIs, depends on Express by calling its methods under the hood, and does not try to optimise routing by using the native uWS router.

That last point is the substantive difference. A library that wraps Express still pays Express's cost, because the request still travels through Express's handler stack before the native layer is reached. A library that reimplements Express's routing directly on the native router removes that layer, which is where the benchmark numbers come from.

The other comparison the README points to is external: three public framework benchmark suites, which test many frameworks under identical conditions and are harder to argue with than a self-published table.

## Apache-2.0 with the Express licence kept alongside, and one tagged release

The licence is Apache-2.0, and there is a second file in the repository root called EXPRESS_LICENSE. That pairing is the correct way to handle a re-implementation of an MIT licensed project, and the package.json files list includes EXPRESS_LICENSE alongside src, which means the Express licence travels with the published package rather than only living in the repository.

The maintenance picture is thin on evidence, and it should be factored into any decision. The repository is not archived, and the last push was on 2026-10-07. The only GitHub release is v2.0.0 from 2025-05-15, while the package.json version reads 2.2.1, and the README refers to a 2.1.0 install as the last version supporting older Node releases. So the npm versions and the tagged releases do not line up, and the project is clearly shipping from the main branch rather than from release tags.

For a package that sits in the request path of a production service, that is the thing to watch. The behaviour-diff test suite is the compensating factor, since it is what makes an unreviewed change to the main branch less likely to change semantics silently. But you are still consuming a package whose version numbers move without a changelog entry per release, and the version numbers here are on the 0.x-style track of a 2.x line where the minor version is where behaviour changes land.

The repository also holds a benchmark/ directory with its own runner, a tests/ directory, an .nycrc.json for coverage configuration, jsconfig.json and tsconfig.json, and an .github directory. There is no separate changelog file in the top level entries, so the GitHub releases page and the commit log are the record.

## Conclusion

Adopt ultimate-express if you have an Express 4 codebase, want to keep the API and the middleware ecosystem, and have measured that request handling is a bottleneck worth removing. Do not adopt it if your application depends on Express internals rather than its public API, or if you have not profiled first, since the routing optimizations only apply to plain string paths. Before you switch, read the differences list, because case sensitive routing is on by default, request bodies are only read for POST, PUT and PATCH, and you must set the UWS_HTTP_MAX_HEADERS_SIZE environment variable if you need headers longer than the uWebSockets default of 4096 bytes.

## FAQ

### How do I switch an Express app to ultimate-express?

Install it with npm install ultimate-express and replace require("express") with require("ultimate-express"). The package requires Node.js 22 or newer; for older Node versions use ultimate-express@2.1.0 for Node 19 to 21 or ultimate-express@node-v18 for Node 18.

### How do I set up HTTPS with ultimate-express?

Instead of creating an https server manually, pass uwsOptions to the express() constructor with key_file_name and cert_file_name, then call app.listen(). The README says the same applies to plain HTTP: do not create the server yourself, use app.listen() instead.

### What are the main differences from Express in ultimate-express?

Case sensitive routing is enabled by default, a new catch async errors option removes the need for express-async-errors, request bodies are only read for POST, PUT and PATCH by default, HTTPS moves into uwsOptions, and the uWebSockets default header limit of 4096 bytes applies unless you set UWS_HTTP_MAX_HEADERS_SIZE.

### Which routes get the routing optimisation in ultimate-express?

Only string paths without regex characters such as *, +, (), {} and so on, and only when case sensitive routing is on, which it is by default. Optimised routes use the native uWS router with a pre-calculated path and can be up to 10 times faster than normal routes.

### How does ultimate-express make sure it behaves like Express?

The README says all tests are run with Express first and then with µExpress, and the results are compared to make sure they match. The project describes itself as a re-implementation rather than a fork, and the package depends on the same middleware modules Express uses.

## Sources

- [dimdenGD/ultimate-express on GitHub](https://github.com/dimdenGD/ultimate-express)
- [Issues](https://github.com/dimdenGD/ultimate-express/issues)
- [License: Apache-2.0](https://github.com/dimdenGD/ultimate-express/blob/main/LICENSE)
- [README](https://github.com/dimdenGD/ultimate-express/blob/main/README.md)
- [Releases](https://github.com/dimdenGD/ultimate-express/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dimdengd-ultimate-express
