The FST_ERR_HOOK_TIMEOUT error occurs when a Fastify hook (onRequest, onSend, etc.) takes longer than the allowed timeout to complete. This error indicates that your hook is either performing long-running operations or is stuck in an infinite loop. Resolving this requires optimizing the hook code or adjusting timeout configuration.
Fixes FST_ERR_HOOK_TIMEOUT
fastify.addHook('onRequest', async (request, reply) => {
// This is an 'onRequest' hook
});
fastify.addHook('preHandler', async (request, reply) => {
// This is a 'preHandler' hook
});fastify.addHook('onRequest', async (request, reply) => {// This is an 'onRequest' hook});fastify.addHook('preHandler', async (request, reply) => {// This is a 'preHandler' hook});Error: FST_ERR_HOOK_TIMEOUT: Hook has exceeded the timeout limit
Fastify enforces timeout limits on hooks to prevent them from hanging indefinitely and blocking request processing. Hooks are middleware-like functions that execute at specific lifecycle points (onRequest, preValidation, preHandler, onSend, etc.). When a hook exceeds its configured timeout threshold without completing, Fastify immediately terminates the hook execution and throws this error. This is a safety mechanism to prevent one slow or broken hook from causing your entire application to hang. The error signals that you have a performance problem or logic bug in your hook implementation that needs to be addressed.
Look at your error logs to see which hook lifecycle stage is failing:
fastify.addHook('onRequest', async (request, reply) => {
// This is an 'onRequest' hook
});
fastify.addHook('preHandler', async (request, reply) => {
// This is a 'preHandler' hook
});Common hook types:
- onRequest: Very first hook, runs before validation
- preValidation: Before request validation
- preHandler: Before route handler executes
- onSend: After handler, before sending response
- onError: When an error occurs
Add logging to identify which hook is timing out:
fastify.addHook('onRequest', async (request, reply) => {
console.log('onRequest hook starting');
// your code
console.log('onRequest hook completed');
});Ensure your hook is not running synchronous blocking code that takes a long time:
// BAD: Synchronous file operations in a hook
fastify.addHook('preHandler', (request, reply, done) => {
const data = fs.readFileSync('./large-file.txt', 'utf-8');
done();
});
// GOOD: Use async file operations
fastify.addHook('preHandler', async (request, reply) => {
const data = await fs.promises.readFile('./large-file.txt', 'utf-8');
});Common blocking operations to avoid:
- fs.readFileSync() / fs.writeFileSync()
- require() for large modules
- Synchronous for loops over large datasets
- Synchronous encryption/hashing
Review your hook code for logic that might hang:
// BAD: Infinite loop possibility
fastify.addHook('preHandler', async (request, reply) => {
while (true) { // Never exits!
await someOperation();
}
});
// BAD: Waiting for a condition that never happens
fastify.addHook('preHandler', async (request, reply) => {
while (!request.user) {
await new Promise(resolve => setTimeout(resolve, 100));
}
});
// GOOD: Use timeouts with Promise.race()
fastify.addHook('preHandler', async (request, reply) => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Hook timeout')), 5000)
);
await Promise.race([yourAsyncOperation(), timeout]);
});If your hook makes database or API calls, ensure they complete quickly:
// BAD: No timeout on external call
fastify.addHook('preHandler', async (request, reply) => {
const user = await db.query('SELECT * FROM users');
});
// GOOD: Add timeout and connection pooling
fastify.addHook('preHandler', async (request, reply) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000); // 3s timeout
try {
const user = await fetch('https://api.example.com/user', {
signal: controller.signal
});
} finally {
clearTimeout(timeout);
}
});
// For databases, ensure connection pooling is configured
const db = knex({
client: 'pg',
connection: {
pool: { min: 2, max: 10 }
}
});If the operation is necessary and takes time, increase the hook timeout in Fastify options:
const fastify = require('fastify')({
requestIdHeader: 'x-request-id',
requestIdLogLabel: 'requestId',
disableRequestLogging: false,
requestTimeout: 30000, // 30 seconds for entire request
});
// For specific hooks, you can't directly configure timeout,
// but you can ensure your operations complete within Fastify's timeout
// Default request timeout is 30 secondsNote: You cannot set per-hook timeouts in Fastify directly. The timeout applies to the entire request. If a hook is slow, you need to optimize it rather than just increase the timeout.
If an operation is legitimately slow, move it outside the critical request path:
// BAD: Heavy processing in a hook blocks the request
fastify.addHook('preHandler', async (request, reply) => {
request.userId = await expensiveUserVerification();
});
// GOOD: Cache the result or defer the work
const userCache = new Map();
fastify.addHook('preHandler', async (request, reply) => {
const cached = userCache.get(request.headers.authorization);
if (cached) {
request.userId = cached;
return;
}
request.userId = 'unknown';
});
// Verify user in background, update cache
setInterval(() => {
updateUserCache();
}, 60000); // Every minuteEnsure your hooks handle errors gracefully:
fastify.addHook('preHandler', async (request, reply) => {
try {
// your hook code
} catch (error) {
// Don't let unhandled errors cause timeouts
fastify.log.error(error);
reply.code(500).send({ error: 'Internal server error' });
}
});
// Or throw and let Fastify's error handler deal with it
fastify.addHook('preHandler', async (request, reply) => {
if (!request.user) {
throw new Error('Unauthorized');
}
});
fastify.setErrorHandler((error, request, reply) => {
fastify.log.error(error);
reply.code(500).send({ error: error.message });
});Add timing instrumentation to measure hook performance:
fastify.addHook('preHandler', async (request, reply) => {
const start = performance.now();
try {
// your hook code
} finally {
const duration = performance.now() - start;
if (duration > 1000) { // Log if over 1 second
fastify.log.warn({ duration }, 'Slow preHandler hook');
}
}
});
// Run your test suite with load testing
npm test
// Use tools like autocannon to load test
npx autocannon -c 10 -d 30 http://localhost:3000Hook execution order: Fastify hooks execute in a specific order: onRequest → preValidation → preHandler → onSend → onResponse. Each hook must complete before the next starts. A slow hook in an early stage (onRequest) will delay all subsequent processing.
Async vs callback syntax: Fastify hooks can use either async/await or callback (done) style. Async is preferred in modern code, but ensure you return a promise or call done():
// Async style
fastify.addHook('preHandler', async (request, reply) => {
await operation();
});
// Callback style
fastify.addHook('preHandler', (request, reply, done) => {
operation().then(() => done()).catch(done);
});Default timeout: Fastify's default request timeout is 30 seconds (requestTimeout: 30000). All hooks must complete within this window combined.
Per-route hooks: You can also use per-route hooks which execute only for specific routes:
fastify.route({
url: '/slow',
method: 'GET',
onRequest: [slowHook],
handler: (request, reply) => {
reply.send({ ok: true });
}
});Debugging: Enable verbose logging to see hook execution:
const fastify = require('fastify')({
logger: {
level: 'debug'
}
});This will show detailed timing information for hook and route execution.