With v0.2.0, my Deno‑based HTTP kernel has taken a decisive step forward: instead of chaining every middleware dynamically for each request, the entire chain is now compiled once. This reduces overhead, simplifies error handling—and makes routing noticeably faster.
Baseline
The first generation of the kernel relied on a recursive dispatch() mechanism:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| private async executePipeline(
ctx: TContext,
middleware: Middleware<TContext>[],
handler: Handler<TContext>,
): Promise<Response> {
const handleInternalError = (ctx: TContext, err?: unknown) =>
this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
ctx,
normalizeError(err),
);
let lastIndex = -1;
const dispatch = async (currentIndex: number): Promise<Response> => {
if (currentIndex <= lastIndex) {
throw new Error('Middleware called `next()` multiple times');
}
lastIndex = currentIndex;
const isWithinMiddleware = currentIndex < middleware.length;
const fn = isWithinMiddleware ? middleware[currentIndex] : handler;
if (isWithinMiddleware) {
if (!isMiddleware(fn)) {
throw new Error('Expected middleware function, but received invalid value');
}
return await fn(ctx, () => dispatch(currentIndex + 1));
}
if (!isHandler(fn)) {
throw new Error('Expected request handler, but received invalid value');
}
return await fn(ctx);
};
try {
const response = await dispatch(0);
return this.cfg.decorateResponse(response, ctx);
} catch (e) {
return handleInternalError(ctx, e);
}
}
|
This approach was easy to understand, but for each request it created several closures, index checks and type checks – all things that waste precious time under high concurrency.
The new approach
When registering a route, a statically linked chain of functions is now generated. The result is a single function runRoute(ctx) that inlines all middlewares and the handler.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| private compile(
route: Omit<IInternalRoute<TContext>, 'runRoute' | 'matcher' | 'method'>,
): (ctx: TContext) => Promise<Response> {
if (!isHandler<TContext>(route.handler)) {
throw new TypeError('Route handler must be a function returning a Promise<Response>.');
}
let composed = route.handler;
for (let i = route.middlewares.length - 1; i >= 0; i--) {
if (!isMiddleware<TContext>(route.middlewares[i])) {
throw new TypeError(`Middleware at index ${i} is not a valid function.`);
}
const current = route.middlewares[i];
const next = composed;
composed = async (ctx: TContext): Promise<Response> => {
let called = false;
return await current(ctx, async () => {
if (called) {
throw new Error(`next() called multiple times in middleware at index ${i}`);
}
called = true;
return await next(ctx);
});
};
}
return composed;
}
|
Important: every middleware still receives its next() function – but thanks to the called flag it can now be invoked only once, reliably preventing multiple executions.
Benchmarks
Measured with 10 000 parallel requests on a Ryzen 7 8845HS:
| Benchmark (parallel) | v0.1.0 (dynamic) | v0.2.0 (pre‑compiled) | Gain |
|---|
| Simple Route | 5.4 ms | 2.1 ms | ≈ 61 % |
| Complex Route | 11.2 ms | 7.9 ms | ≈ 29 % |
For single requests, the numbers are almost identical – the speed advantage becomes apparent mainly under heavy load.
Conclusion
- Static routing reduces overhead and significantly increases throughput.
next() errors are now detected immediately.- The API remained compatible – upgrading from
0.1.x to 0.2.0 requires no code changes.
The kernel is already running fast and stable in my build‑cache‑server – a Deno‑based build cache for act; compatible with the GitHub Actions Cache API.
👉 Source code & release notes: https://github.com/0xMax42/http-kernel