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.
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.
Error: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
Error: Middleware next() called multiple times (next() invoked twice)
Express middleware next() called multiple times
Error: Worker failed to initialize (worker startup error)
Worker failed to initialize in Node.js
Error: EMFILE: too many open files, open 'file.txt'
EMFILE: too many open files
Error: cluster.fork() failed (cannot create child process)
cluster.fork() failed - Cannot create child process