Fastify 5.0: Performance Upgrades and Async Hooks Deep Dive
Fastify 5.0 brings major performance improvements, enhanced async hook support, and breaking changes. Learn what's new, how to migrate, and how to leverage the updates.
Introduction
Fastify 5.0 has arrived, and it represents a significant milestone for the high-performance Node.js web framework. With a focus on async hook improvements, performance optimizations, and modernization, this release sets a new standard for building fast, reliable APIs.
Whether you’re running a greenfield project or maintaining legacy services, understanding the changes in Fastify 5.0 is critical for staying current and maximizing performance.
What’s New in Fastify 5.0
Major Performance Gains
Fastify 5.0 delivers measurable throughput improvements across the board:
- 20–30% faster request handling in typical REST API scenarios
- Reduced memory footprint through internal optimization and garbage collection improvements
- Faster plugin loading and initialization via streamlined dependency resolution
- Better handling of high-concurrency workloads with improved async resource tracking
These gains come from:
- Optimized route matching — The router has been refactored to reduce lookup times, especially for routes with many parameters.
- Improved async context tracking — Better integration with Node.js’s AsyncLocalStorage and AsyncResource.
- Reduced JSON serialization overhead — Smarter JSON handling for large payloads.
- Native support for HTTP/2 Server Push — Previously an extension; now built-in and optimized.
Enhanced Async Hooks
Async hooks are central to Fastify’s plugin ecosystem. Fastify 5.0 refines these significantly:
onRequest → onRequestHookHandler
The hook signature is now more explicit and type-safe:
fastify.addHook('onRequest', async (request, reply) => {
console.log(`[${request.method}] ${request.url}`);
// Hook logic here
});
New onSend Hook Timing
The onSend hook now fires before reply object serialization, allowing you to intercept and modify the response object directly:
fastify.addHook('onSend', async (request, reply, payload) => {
// Payload is the serialized response
// Modify headers, logging, metrics — all happen before send
reply.header('X-Response-Time', Date.now() - request.startTime);
return payload;
});
New onError Hook
Better error handling with the enhanced onError hook:
fastify.addHook('onError', async (request, reply, error) => {
console.error(`Error on ${request.method} ${request.url}:`, error.message);
// Custom error logging, metrics, alerting
reply.status(error.statusCode || 500).send({ error: error.message });
});
Lifecycle Guarantees
Fastify 5.0 now guarantees hook execution order, even in error scenarios:
const lifecycleExample = async () => {
// 1. onRequest
fastify.addHook('onRequest', async (request, reply) => {
console.log('1. onRequest fired');
});
// 2. preHandler (route-specific)
fastify.addHook('preHandler', async (request, reply) => {
console.log('2. preHandler fired');
});
// 3. Route handler
fastify.get('/example', async (request, reply) => {
console.log('3. Route handler running');
return { status: 'ok' };
});
// 4. onSend
fastify.addHook('onSend', async (request, reply, payload) => {
console.log('4. onSend fired');
return payload;
});
// 5. onResponse
fastify.addHook('onResponse', async (request, reply) => {
console.log('5. onResponse fired');
});
};
This predictable flow is essential for metrics, logging, and middleware-like behavior.
Breaking Changes You Need to Know
1. Node.js Version Requirement
Fastify 5.0 requires Node.js 18+. If you’re on Node.js 16 or earlier, you must upgrade.
2. Deprecated Plugins Removed
Several long-deprecated plugins have been removed:
-
fastify-jwt→ Use@fastify/jwt -
fastify-cors→ Use@fastify/cors -
fastify-helmet→ Use@fastify/helmet
Update your imports:
// Old (no longer works)
import fastifyJwt from 'fastify-jwt';
// New
import fastifyJwt from '@fastify/jwt';
fastify.register(fastifyJwt, { secret: 'your-secret' });
3. Changed Reply.send() Behavior
The reply.send() method now throws if called multiple times in the same request:
fastify.get('/bad', async (request, reply) => {
reply.send({ data: 1 });
reply.send({ data: 2 }); // Throws FasifyError
});
4. Stricter Schema Validation
JSON Schema validation is now strict by default. Invalid schemas will throw during route registration:
// This will now throw during app startup
fastify.post('/data', {
schema: {
body: {
type: 'object',
properties: {
name: { type: 'invalid-type' } // Error!
}
}
},
handler: async (request, reply) => {
return { ok: true };
}
});
5. Plugin Load Order
Plugin dependency order is now enforced. Plugins that declare dependencies on other plugins will throw if those plugins aren’t registered first:
// plugin-b.js
export default async function pluginB(fastify, opts) {
// Declares dependency on plugin-a
fastify.register(pluginA);
}
pluginB.dependencies = ['plugin-a']; // Explicit dependency
Step-by-Step Migration Guide
Step 1: Audit Your Dependencies
Check your package.json for deprecated plugin usage:
npm ls | grep fastify-
Replace old packages with their scoped equivalents:
npm uninstall fastify-cors fastify-jwt fastify-helmet
npm install @fastify/cors @fastify/jwt @fastify/helmet
Step 2: Update Node.js
Ensure you’re on Node.js 18+:
node --version
# Should output v18.0.0 or higher
Step 3: Update Fastify
npm install [email protected]
Step 4: Test Schema Validation
Run your test suite to catch schema validation errors early:
npm test
Step 5: Update Hook Usage
Review your hooks and update them to use the new signatures if needed. For most cases, existing code will work without changes, but test thoroughly.
Example Migration: JWT Plugin
Before (Fastify 4.x):
import Fastify from 'fastify';
import fastifyJwt from 'fastify-jwt';
const fastify = Fastify();
fastify.register(fastifyJwt, {
secret: process.env.JWT_SECRET,
sign: { expiresIn: '24h' }
});
fastify.get('/protected', async (request, reply) => {
await request.jwtVerify();
return { data: 'protected' };
});
After (Fastify 5.0):
import Fastify from 'fastify';
import fastifyJwt from '@fastify/jwt';
const fastify = Fastify();
await fastify.register(fastifyJwt, {
secret: process.env.JWT_SECRET,
sign: { expiresIn: '24h' }
});
fastify.get('/protected', async (request, reply) => {
await request.jwtVerify();
return { data: 'protected' };
});
The main change: use the scoped package name.
Performance Testing
Benchmarking Your Migration
Use a tool like autocannon to compare performance before and after:
npm install -D autocannon
// benchmark.js
import autocannon from 'autocannon';
const result = await autocannon({
url: 'http://localhost:3000/api/endpoint',
connections: 100,
duration: 10,
requests: [
{
path: '/api/data',
method: 'GET'
}
]
});
console.log('Throughput:', result.requests.average, 'req/sec');
console.log('Latency (p99):', result.latency.p99, 'ms');
Run this before and after upgrading to quantify gains.
Common Pitfalls
Pitfall 1: Multiple reply.send() Calls
Fastify 5.0 enforces single-send semantics. If you have code like:
fastify.get('/data', async (request, reply) => {
if (!request.query.id) {
reply.status(400).send({ error: 'Missing id' });
}
const data = await fetchData(request.query.id);
reply.send(data); // This will throw if id was missing
});
Fix with early returns:
fastify.get('/data', async (request, reply) => {
if (!request.query.id) {
return reply.status(400).send({ error: 'Missing id' });
}
const data = await fetchData(request.query.id);
return reply.send(data);
});
Pitfall 2: Forgetting await on Plugin Registration
In Fastify 5.0, plugin registration should be awaited in async contexts:
// ❌ Bad
const app = Fastify();
app.register(myPlugin);
app.listen({ port: 3000 });
// ✅ Good
const app = Fastify();
await app.register(myPlugin);
await app.listen({ port: 3000 });
Pitfall 3: Schema Validation Errors
Invalid JSON schemas will now throw immediately. Use a JSON schema validator like JSON Formatter to validate your schemas before deploying:
// Validate schema structure
const isValidSchema = (schema) => {
try {
// Compile the schema
const ajv = new Ajv();
ajv.compile(schema);
return true;
} catch (e) {
console.error('Invalid schema:', e.message);
return false;
}
};
Why It Matters
Fastify 5.0 is more than a version bump—it’s a consolidation of best practices:
- Performance — Every millisecond matters in production. The 20–30% improvements translate to reduced infrastructure costs and better user experience.
- Reliability — Stricter validation and hook guarantees make debugging easier and production issues rarer.
- Modern JavaScript — Requiring Node.js 18+ lets Fastify leverage modern async/await patterns and native APIs.
- Plugin Ecosystem — Unified scoped package naming makes it easier to discover and manage official plugins.
Getting Started
Quick Start Example
Here’s a complete Fastify 5.0 server:
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
// Add a hook
fastify.addHook('onRequest', async (request, reply) => {
request.startTime = Date.now();
});
fastify.addHook('onResponse', async (request, reply) => {
const duration = Date.now() - request.startTime;
fastify.log.info({ duration, method: request.method, url: request.url });
});
// Simple route
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
// Route with validation
fastify.post('/data', {
schema: {
body: {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' }
},
required: ['name', 'email']
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' }
}
}
}
},
handler: async (request, reply) => {
// request.body is already validated
return { id: '123', name: request.body.name };
}
});
// Start server
await fastify.listen({ port: 3000 });
console.log('Server running on http://localhost:3000');
Testing Your Routes
Use the API Request Builder to test your routes during development:
curl -X POST http://localhost:3000/data \
-H "Content-Type: application/json" \
-d '{"name": "John", "email": "[email protected]"}'
Handling JWT Authentication
If your API uses JWT tokens, Fastify 5.0’s improved async hooks make authentication cleaner:
import Fastify from 'fastify';
import fastifyJwt from '@fastify/jwt';
const fastify = Fastify();
await fastify.register(fastifyJwt, {
secret: process.env.JWT_SECRET || 'super-secret'
});
// Protect routes with a preHandler hook
fastify.addHook('preHandler', async (request, reply) => {
if (request.url.startsWith('/protected')) {
try {
await request.jwtVerify();
} catch (error) {
reply.status(401).send({ error: 'Unauthorized' });
}
}
});
fastify.post('/login', async (request, reply) => {
const token = fastify.jwt.sign({ userId: 'user123' });
return { token };
});
fastify.get('/protected', async (request, reply) => {
// Only reached if JWT is valid
return { data: 'secret', userId: request.user.userId };
});
You can verify your JWT tokens using the JWT Decoder tool during development.
Debugging with Kloubot Tools
When building Fastify 5.0 applications, several Kloubot tools are invaluable:
- JSON Formatter — Validate your route schemas and response payloads
- JWT Decoder — Decode and inspect JWT tokens without leaving your browser
- Hash Generator — Generate secure password hashes for testing
- API Request Builder — Send requests to your API and inspect responses
- Webhook Tester — Capture and debug webhook payloads from external services
Conclusion
Fastify 5.0 represents a significant step forward for Node.js developers building high-performance APIs. With improved async hooks, guaranteed performance gains, and a more predictable plugin ecosystem, migrating from earlier versions is worth the effort.
The breaking changes are minimal and well-documented. By following the migration guide and testing thoroughly, you can be up and running in hours, not days.
Start with the examples in this post, run your test suite, and measure the performance improvements. Your users will thank you.