Performance Tuning
Optimize NextRush applications for production throughput — Node.js runtime flags, hot-path hygiene, connection handling, and middleware ordering.
How to get the most out of NextRush in production. These techniques apply regardless of your deployment target.
Middleware order matters
Middleware runs on every request. Put fast-exit middleware first so rejected requests skip expensive processing.
// ✅ Optimal order — reject early, process late
app.use(rateLimit()); // 1. Reject abusive clients immediately
app.use(cors()); // 2. Reject disallowed origins
app.use(helmet()); // 3. Set security headers (cheap)
app.use(requestId()); // 4. Tag request for tracing
app.use(bodyParser()); // 5. Parse body (expensive — only for allowed requests)
app.use(authMiddleware); // 6. Auth check
app.route('/api', router); // 7. Business logic// ❌ Wasteful order — parses body before checking rate limit
app.use(bodyParser()); // Parses body for every request
app.use(rateLimit()); // Then rejects — wasted workThe earlier a middleware can reject a request, the earlier it should run.
Use route-specific middleware
Don't apply expensive middleware globally when only some routes need it.
// ❌ Global body parsing — even GET requests parse (no-op but adds overhead)
app.use(bodyParser());
router.get('/health', healthCheck);
router.post('/users', createUser);
// ✅ Route-specific — body parsing only where needed
router.get('/health', healthCheck);
router.post('/users', bodyParser(), createUser);Routing performance
NextRush's router is a segment trie with O(k) lookup, where k is the number of path
segments in the request path — not the number of registered routes. Route count does not
affect matching speed. This is documented and test-verified in
packages/router/README.md
("O(k) route matching where k = path segment count, not route count") and audited in
packages/router/ROUTER_AUDIT.md
(200 router tests, correctness-focused, not a performance benchmark). For the mental model and
matching algorithm, see Concepts: Routing.
Avoiding blocking I/O in hot paths
The request-handling path (middleware chain, router lookup) runs on Node's single event loop.
Synchronous, CPU-bound work or blocking I/O (synchronous file reads, crypto sync APIs, tight
loops) in a middleware or handler stalls every other in-flight request on that process. Prefer
the async/await-native APIs already used throughout NextRush's own middleware, and offload
genuinely CPU-heavy work (image processing, heavy compression) to a worker thread or a separate
service rather than the request path.
JSON response optimization
Return objects directly (class-based)
In class-based controllers, return values are auto-serialized. Avoid double-serialization.
// ✅ Return object — NextRush serializes once
@Get()
async findAll() {
return this.users.findAll();
}
// ❌ Manual JSON — serializes twice (you + NextRush)
@Get()
async findAll(@Ctx() ctx) {
ctx.json(await this.users.findAll());
}Pre-compute static responses
For responses that don't change per-request:
const HEALTH_RESPONSE = JSON.stringify({ status: 'ok' });
router.get('/health', (ctx) => {
ctx.set('content-type', 'application/json');
ctx.send(HEALTH_RESPONSE);
});This avoids JSON.stringify on every health check.
Connection handling
Keep-alive
Node.js enables keep-alive by default. Ensure your reverse proxy preserves it:
# nginx.conf
upstream api {
server 127.0.0.1:8080;
keepalive 64;
}
server {
location /api/ {
proxy_pass http://api;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}Cluster mode
Use Node.js cluster to utilize all CPU cores:
import cluster from 'node:cluster';
import os from 'node:os';
if (cluster.isPrimary) {
const cpuCount = os.cpus().length;
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} died, restarting`);
cluster.fork();
});
} else {
await import('./index.js');
}Linear throughput scaling up to CPU core count is the expected impact — a 4-core machine should handle ~4x single-process RPS.
Connection pooling
NextRush's core has no database client — connection pooling is the responsibility of whichever
client library you pair with a data store (e.g. pg.Pool for PostgreSQL, a Redis client's
built-in pool). The pattern that applies regardless of client: create the pool once at startup
(module scope, outside any request handler) and inject or import the shared instance — never
open a new connection per request. This mirrors the framework's own DI singleton pattern (see
Concepts: Dependency Injection) — a database pool is a
natural fit for a singleton-scoped service.
Memory management
Avoid global caches without bounds
// ❌ Unbounded cache — grows forever
const cache = new Map<string, unknown>();
// ✅ LRU-style bounded cache
const MAX_CACHE = 1000;
const cache = new Map<string, unknown>();
function setCache(key: string, value: unknown) {
if (cache.size >= MAX_CACHE) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, value);
}Stream large responses
Don't load entire files into memory. Use ctx.stream() with a Node.js Readable or Web ReadableStream:
import { createReadStream } from 'node:fs';
router.get('/download/:file', async (ctx) => {
ctx.set('content-type', 'application/octet-stream');
const stream = createReadStream(`./files/${ctx.params.file}`);
await ctx.stream(async (writer) => {
for await (const chunk of stream) {
await writer.write(chunk as Buffer);
}
});
});ctx.send() also accepts a Node.js Readable or Web ReadableStream directly and pipes it — use ctx.stream() when you need explicit chunk-by-chunk control (backpressure, cancellation via writer.signal).
Production configuration
Environment variables
NODE_ENV=production # Enables production optimizations
UV_THREADPOOL_SIZE=16 # Increase for I/O-heavy apps (default: 4)
NODE_OPTIONS="--max-old-space-size=2048" # Set heap limitGraceful shutdown
Handle SIGTERM so in-flight requests complete:
const server = await listen(app, 8080);
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully...');
// Force shutdown after 30 seconds if connections don't drain in time
const forceExit = setTimeout(() => {
console.error('Forced shutdown');
process.exit(1);
}, 30000);
await server.close();
clearTimeout(forceExit);
console.log('All connections closed');
process.exit(0);
});Benchmarking your application
Measure before and after every optimization.
# Quick check with autocannon
npm install -g autocannon
autocannon -c 64 -d 10 http://localhost:8080/api/users
# Full suite
cd apps/benchmark
pnpm install
pnpm benchSee Benchmarking for methodology and how publishable numbers are produced.
Summary
| Technique | Impact | Effort |
|---|---|---|
| Middleware ordering | 5–20% RPS gain | Low |
| Route-specific middleware | 5–15% on affected routes | Low |
| Pre-computed JSON | 10–30% on static endpoints | Low |
| Cluster mode | ~Nx (N = CPU cores) | Medium |
| Stream large payloads | Prevents OOM, reduces latency | Medium |
| Bounded caches | Prevents memory leaks | Low |
| Graceful shutdown | Zero dropped requests | Low |
Next steps
- Benchmark methodology: Benchmarking
- Router internals: Concepts: Routing