Elysia Performance: Why It’s Fast by Default
Elysia is a high-performance HTTP framework built for Bun. Its design philosophy centres on doing as much work as possible at startup time so that request processing is minimal and predictable. By leveraging Ahead-of-Time (AOT) compilation, static analysis, and tight integration with Bun’s native APIs, Elysia delivers throughput that often exceeds traditional Node.js frameworks by an order of magnitude. Understanding these foundations is the first step to writing truly performant applications.
How Elysia achieves speed
Three pillars underpin Elysia’s performance:
- AOT compilation of schemas – validation functions for
params,query, andbodyare compiled into pure JavaScript closures during startup. No generic JSON schema evaluation happens at runtime. - Radix tree routing – routes are indexed into a compressed radix tree, enabling O(1) lookup even with thousands of routes and dynamic segments.
- Minimal allocation – the framework avoids unnecessary object creation, using Bun’s fast
Request/Responseobjects and zero-copy patterns where possible.
Why Performance Matters in Modern Web Applications
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →High throughput and low latency directly translate to better user experience and lower infrastructure costs. In serverless environments (AWS Lambda, Vercel Edge Functions), cold start time is critical—Elysia’s compile step can be run at build time, keeping cold starts under 50 ms. For always-on servers, higher requests per second mean you can handle more traffic with fewer instances, reducing your cloud bill.
Beyond raw numbers, performance optimization often leads to cleaner code. When you define strict schemas and minimise unnecessary middleware, you produce applications that are easier to reason about, test, and maintain.
Core Optimization Techniques
The following techniques are proven to squeeze maximum performance out of your Elysia services. Each technique includes a practical code example.
1. Leverage Schema Validation and AOT Compilation
Every time you omit a schema, Elysia falls back to manual runtime validation—or none at all—which can lead to hidden bugs and slower execution. By providing schemas using the built-in @elysiajs/typebox (or simply t), you enable compile-time validation. The validator becomes a dedicated function with no runtime overhead of a generic validator.
import { Elysia, t } from 'elysia';
// ❌ Slow: manual parsing + no validation
new Elysia()
.get('/user/:id', ({ params }) => {
const id = parseInt(params.id);
// risk of NaN, missing checks, slower response
return { user: id };
});
// ✅ Fast: AOT-compiled validator from schema
new Elysia()
.get('/user/:id', ({ params }) => {
return { user: params.id };
}, {
params: t.Object({
id: t.Numeric()
})
});
The t.Numeric() schema ensures that params.id is already a number when your handler runs. No parsing inside the handler, no extra allocations—just direct access.
2. Use Static Routes Where Possible
Dynamic path segments (e.g., /post/:slug) are powerful but require parameter extraction. If you can move a variable to a query parameter, you keep the route static, which avoids segment parsing entirely.
// Dynamic route: extra parsing cost
app.get('/post/:slug', ({ params }) => fetchPost(params.slug));
// Static route with query param – faster path lookup
app.get('/post', ({ query }) => fetchPost(query.slug), {
query: t.Object({
slug: t.String()
})
});
Static routes are stored in a flat map inside the radix tree, resulting in the fastest possible dispatch.
3. Minimize Middleware Overhead
Each middleware function adds to the per-request call stack. While Elysia’s lifecycle is lightweight, avoid blanket middleware that runs on every route. Instead, apply middleware locally to specific groups or routes.
const app = new Elysia();
// ❌ Global middleware affects all routes
app.use(async (context) => {
// logging or auth that runs for every request
console.log('Request received');
});
// ✅ Scoped middleware on a group
const protected = app.group('/admin', (group) =>
group.use(isAdmin) // only for /admin/*
.get('/dashboard', () => 'Admin panel')
);
Also prefer synchronous middleware when possible. Elysia’s use() supports both sync and async; synchronous functions avoid the microtask overhead of promises.
4. Optimize Lifecycle Hooks (derive and transform)
derive and transform are powerful hooks that run before the main handler. However, they introduce extra function calls. Keep them lean: derive only the values you truly need, and avoid expensive operations like database calls unless they are cached.
// Inefficient: derive runs an external API call on every request
app.derive(({ headers }) => {
const user = await fetchUser(headers.authorization); // costly
return { user };
}).get('/profile', ({ user }) => user.name);
// Efficient: derive just extracts token, handler calls the service
app.derive(({ headers }) => {
const token = headers.authorization?.split(' ')[1];
return { token };
}).get('/profile', async ({ token }) => {
const user = await fetchUser(token); // only when route matches
return user.name;
});
This pattern ensures that heavy work happens only in the matched route, not in every request that passes through the hook.
5. Production Compilation with compile()
During development you call .listen() to start a server. For production, Elysia offers a dedicated .compile() method that pre-compiles all routes, schemas, and hooks into an optimised fetch handler. You then pass this handler directly to Bun.serve(), eliminating any framework overhead at startup.
import { Elysia, t } from 'elysia';
const app = new Elysia()
.get('/health', () => ({ status: 'ok' }))
.post('/submit', ({ body }) => ({ id: crypto.randomUUID() }), {
body: t.Object({
name: t.String()
})
})
.compile(); // returns a highly optimized Request → Response function
Bun.serve({
fetch: app.fetch,
port: 3000
});
The compiled handler skips all runtime introspection, making it ideal for serverless deployments where you can even serialize the compiled handler as part of your build artifact.
6. Use Bun’s Native Features Directly
Elysia sits on top of Bun, so you can always drop down to native APIs for static file serving, WebSocket handling, or streaming. The @elysiajs/static plugin uses Bun’s Bun.file() and sendFile under the hood, which are zero-copy. For WebSockets, Bun’s built-in support is faster than any framework abstraction.
import { Elysia } from 'elysia';
import { staticPlugin } from '@elysiajs/static';
const app = new Elysia()
.use(staticPlugin({ assets: 'public', prefix: '/static' }))
.ws('/ws', {
message(ws, message) {
ws.send(`Echo: ${message}`);
}
})
.compile();
When serving static assets, enable caching headers via the plugin options to avoid repeated disk reads.
Benchmarking Elysia Applications
To make informed optimization decisions, you need to measure. Use modern HTTP benchmarking tools like oha, bombardier, or wrk. Always run benchmarks in the same environment (Linux, same CPU) and for a consistent duration.
Example Benchmark with oha
First, start your Elysia server (development mode is fine for initial tests, but final benchmarks should use the compiled handler).
bun run server.ts
Then run a benchmark:
oha -n 200000 -c 100 http://localhost:3000/health
Typical output on a modern laptop (M1/M2 or Ryzen):
Summary:
Success rate: 100.00%
Total: 200000 requests
Duration: 1.2 seconds
Requests/sec: 166666.67
Avg latency: 0.6 ms
P99 latency: 1.2 ms
Compare this to a Node.js + Express baseline, which often peaks around 20k–30k req/s on the same hardware. The difference comes from the AOT schema compilation and Bun’s event loop.
Profiling individual routes
Use Elysia’s built-in onRequest / onResponse lifecycle hooks to log timing for specific routes. This helps you identify bottlenecks without external tools.
const app = new Elysia()
.onRequest(({ request }) => {
request.startTime = performance.now();
})
.onResponse(({ request, set }) => {
const duration = performance.now() - request.startTime;
set.headers['X-Response-Time'] = `${duration.toFixed(2)}ms`;
})
.get('/slow', async () => {
await Bun.sleep(50); // simulated work
return 'done';
});
Check the X-Response-Time header or aggregate it via a logging sink to find slow routes.
Best Practices for Peak Performance
Follow these guidelines to keep your Elysia application as fast as possible:
- Always define schemas – for
params,query,body, andheaders. The AOT compiler turns them into the fastest possible validators. - Use
compile()in production – never run.listen()for live traffic; the compiled handler is consistently 20-30% faster. - Keep handlers synchronous where feasible – async functions incur microtask overhead. If your logic is CPU-bound and quick (e.g., in-memory calculation), keep it sync.
- Scope middleware – apply authentication, logging, or parsing only to the routes that need them using
.group(). - Leverage native Bun features – use
Bun.file()for static files,Bun.write()for streaming, and WebSocket directly when possible. - Avoid deep cloning in
derive– return only what’s necessary and don’t duplicate entire request objects. - Benchmark before and after changes – use tools like
ohato verify that your optimization actually improves throughput or reduces latency. - Cache aggressively – use Elysia’s built-in
cache()plugin or setCache-Controlheaders for static assets to offload your server.
Conclusion
Elysia’s architecture provides an exceptionally fast baseline, but the real performance wins come from understanding and applying its optimization techniques. By defining strict schemas, minimising middleware, compiling for production, and using Bun’s native capabilities, you can build services that handle hundreds of thousands of requests per second with sub-millisecond latency. These practices not only improve speed but also result in cleaner, more maintainable codebases. Start benchmarking your own routes, apply the techniques above, and watch your application’s performance soar.